Práce se soubory v HTML5

Pomocí souborového API, které bylo přidáno k DOM v HTML5, může webová aplikace požádat uživatele, aby vybral lokální soubory, a poté přečíst jejich obsah. Uživatel může soubory vybrat buď klasicky pomocí HTML elementu input, nebo pomocí techniky drag-and-drop, tedy přetažením do okna webového prohlížeče.
Nálepky:
Článek je překladem textu Using files from web applications, který je k dispozici na webu Mozilla Developer Center pod licencí CC-BY-SA. Tento text je k dispozici pod stejnou licencí, můžete jej tedy šířit a upravovat, pokud zachováte informace o autorovi a pokud své dílo zveřejníte pod podobnou licencí.
Poznámka překladatele
Text pojednává o technice, která byla představena v HTML5 a kterou podporují nejnovější webové prohlížeče. S její pomocí lze snadno provádět některé operace se soubory, které bylo až dosud nutno řešit pomocí pluginů (Flash, Java), přímo z HTML a JavaScriptu. Pravděpodobně nejznámější implementací této techniky je přidávání příloh do mailů pomocí drag-and-drop, které před několika dny představil GMail.
Výběr souborů pomocí HTML
Vybrat jeden soubor a použít jej pomocí File API je snadné:
<input type="file" id="input" onchange="handleFiles(this.files)" />
Jakmile uživatel vybere soubor, je zavolána funkce handleFiles()
, jako parametr je jí předán objekt FileList
, a v něm je objekt File
, který představuje soubor vybraný uživatelem.
Pokud chcete povolit uživateli vybrat víc souborů najednou, použijte atribut multiple
:
<input type="file" id="input" multiple="true" onchange="handleFiles(this.files)" />
V takovém případě bude seznam souborů, předaný funkci handleFiles()
, obsahovat samostatný objektFile
pro každý soubor, který uživatel vybral.
Dynamické přidání obslužné rutiny
Pokud jste vytvořili vstupní pole pomocí JavaScriptové knihovny, např. jQuery, budete muset k přidání obslužné funkce použít element.addEventListener()
, například takto:
var inputElement = document.getElementById("inputField"); inputElement.addEventListener("change", handleFiles, false); function handleFiles() { var fileList = this.files; /* zde můžete pracovat se seznamem souborů */ }
Všimněte si, že v takovém případě není předán seznam souborů funkci handleFiles()
jako parametr, ale je v this, protože obslužné rutiny událostí, které jsou přidané tímto způsobem, nedostávají data coby parametry při volání funkce.
Výběr souborů pomocí drag-and-drop
Uživatelům své webové aplikace můžete rovněž nabídnout možnost přetažení souborů z prostoru operačního systému (z plochy, Průzkumníka atd.) na určené místo.
První krok je vytvoření oblasti, do níž budou soubory přetahovány („drop zone“). Přesné určení místa, kam mají být soubory přetaženy, záleží na designu vaší aplikace. Vlastní vytvoření elementu, který přijímá událost „puštění“, je snadné:
var dropbox; dropbox = document.getElementById("dropbox"); dropbox.addEventListener("dragenter", dragenter, false); dropbox.addEventListener("dragover", dragover, false); dropbox.addEventListener("drop", drop, false);
V tomto příkladu jsme udělali z elementu s ID „dropbox“ oblast, která přijímá puštěné objekty. Stačí nastavit obsluhu událostí dragenter
, dragover
a drop
.
V našem případě nepotřebujeme nijak zvlášť ošetřovat události dragenter
a dragover
, tudíž budou obě obslužné funkce velmi jednoduché. Pouze zastavíme další šíření události a zabráníme tomu, aby byly vykonány standardní akce:
function dragenter(e) { e.stopPropagation(); e.preventDefault(); } function dragover(e) { e.stopPropagation(); e.preventDefault(); }
Celé kouzlo se skrývá ve funkci drop()
:
function drop(e) { e.stopPropagation(); e.preventDefault(); var dt = e.dataTransfer; var files = dt.files; handleFiles(files); }
V této funkci nejprve vyzvedneme pole dataTransfer
z objektu události, z něho pak vybereme seznam souborů, a ten předáváme funkci handleFiles()
ke zpracování. Od tohoto okamžiku je zpracování naprosto stejné jako ve výše uvedeném příkladu s elementem input
.
Získání informací o souborech
ObjektFileList
obsahuje seznam všech souborů, které uživatel vybral, každý jako samostatný objekt File
. Můžete snadno zjistit jejich celkový počet stejně jako u ostatních seznamů, pomocí čtení atributu length
:
var numFiles = files.length;
Jednotlivé objekty typuFile
získáme známým způsobem – přístupem k seznamu jako k poli:
for (var i = 0; i < files.length; i++) { var file = files[i]; .. }
Tento cyklus projde všechny soubory v seznamu.
ObjektFile
poskytuje několik atributů, které obsahují užitečné informace o souboru – nás budou zajímat především tyto tři:
name
- Jméno souboru (jen ke čtení). Je to pouze čisté jméno souboru a neobsahuje žádné informace o jeho umístění na disku (je bez cesty).
size
- Velikost souboru v bajtech jako 64bitové celé číslo bez znaménka (pouze ke čtení).
type
- MIME typ souboru jako řetězec (pouze ke čtení), nebo prázdný řetězec, pokud nemohl být MIME typ zjištěn.
Příklad: Zobrazení náhledů vybraných obrázků
Řekněme, že vyvíjíte další úžasný web pro sdílení fotografií a chcete použít HTML5 pro zobrazení náhledů obrázků dřív, než je uživatel uploaduje na server. Stačí vám připravit element input nebo „drop zone“, viz výše, a k obsluze zavolat funkci, která bude podobná této:
function handleFiles(files) { for (var i = 0; i < files.length; i++) { var file = files[i]; var imageType = /image.*/; if (!file.type.match(imageType)) { continue; } var img = document.createElement("img"); img.classList.add("obj"); img.file = file; preview.appendChild(img); var reader = new FileReader(); reader.onload = (function(aImg) { return function(e) { aImg.src = e.target.result; }; })(img); reader.readAsDataURL(file); } }
V cyklu procházíme seznam souborů, které uživatel vybral, a kontrolujeme, zda jejich typ (atribut type
) odpovídá obrázku (pomocí regulárního výrazu, který hledá řetězec „image.*“). Pro každý takový soubor vytvoříme nový element img
element. Pomocí CSS mu můžeme nastavit nějaké hezké okraje, stínování a určit velikost, ale pro naši ukázku to není nezbytné.
Každému obrázku jsme přiřadili CSS třídu „obj“, aby byly snadno k nalezení. Rovněž jsme každému nastavili atribut file
a do něho uložili objektFile
pro daný obrázek; to nám později dovolí vybrat soubory k uploadu. Nakonec jsme pomocí Node.appendChild()
přidali nový element do dokumentu.
Poté jsme vytvořili instanci FileReader
, který se postará o asynchronní načtení obrázku a připojení k elementu img
. Po vytvoření nového objektu FileReader
nastavujeme obsluhu pro jeho událost onload
a voláme metodu readAsDataURL()
, která zahájí čtení dat na pozadí. Když jsou veškerá data načtena, převede je na URL ve tvaru data:
, a to předá obslužné funkci pro událost onload
. Naše implementace této obsluhy nastaví atribut src
u elementu img
na předané URL. Výsledkem je, že se obrázek objeví na stránce.
Příklad: Upload vybraného souboru
Další věcí, kterou můžete chtít udělat, je dát uživateli možnost nahrát vybraný soubor nebo soubory na server. V HTML5 to lze udělat velmi snadno „na pozadí“.
Pozor! Následující text používá metody, konkrétně getAsBinary(), které fungují v prohlížečích s jádrem Gecko a nejsou součástí specifikace File API od W3C, takže jsou „deprecated“. Rozhodně proto nelze doporučit popisované řešení jako implementační standard, spíš jako inspiraci k vytvoření vlastní obdoby – pozn.překl.
Příprava dat pro upload
Budeme pokračovat v předchozím příkladu, který zobrazoval náhledy obrázků. Jednotlivé obrázky zobrazoval jako elementy img
s CSS třídou „obj“ a do atributu file
ukládal odpovídající objekt File
. To nám umožní snadno získat všechny obrázky, které uživatel vybral k uploadu, pomocí Document.querySelectorAll()
– například takto:
function sendFiles() { var imgs = document.querySelectorAll(".obj"); for (var i = 0; i < imgs.length; i++) { new FileUpload(imgs[i], imgs[i].file); } }
Na řádku 2 nám metoda querySelectorAll vrátí pole, které si uložíme do proměnné imgs
, a v něm budou všechny elementy z aktuálního dokumentu, které mají CSS třídu „obj“. Což budou v našem případě právě ty dříve vybrané obrázky. Jakmile máme tento seznam připravený, můžme jej projít a pro každou položku vytvořit novou instanci FileUpload
, která se postará o odeslání souboru.
Obsluha uploadu pro jeden soubor
Funkce FileUpload
dostává dva argumenty: element img a soubor, z něhož bude číst data.
function FileUpload(img, file) { this.ctrl = createThrobber(img); var xhr = new XMLHttpRequest(); this.xhr = xhr; var self = this; this.xhr.upload.addEventListener("progress", function(e) { if (e.lengthComputable) { var percentage = Math.round((e.loaded * 100) / e.total); self.ctrl.update(percentage); } }, false); xhr.upload.addEventListener("load", function(e){ self.ctrl.update(100); var canvas = self.ctrl.ctx.canvas; canvas.parentNode.removeChild(canvas); }, false); xhr.open("POST", "http://demos.hacks.mozilla.org/paul/demos/resources/webservices/devnull.php"); xhr.overrideMimeType('text/plain; charset=x-user-defined-binary'); xhr.sendAsBinary(file.getAsBinary()); }
Výše uvedená funkce FileUpload()
nejprve vytváří throbber (ukazatel), který zobrazuje průběh nahrávání, a pak pomocíXMLHttpRequest
nahrává data na server.
Před samotným odesláním dat na server probíhají ještě některé přípravné kroky:
- Je nastavena obsluha události „progress“ u
XMLHttpRequest
tak, aby aktualizovala ukazatel postupu. Ukazatel bude tedy zobrazovat průběžně aktuální data. - Obsluha události „load“ pro
XMLHttpRequest
se stará o to, aby zobrazila v ukazateli hodnotu „100%“, a pak ukazatel odstraní. - Požadavek na upload je vytvořen zavoláním metody
open()
objektuXMLHttpRequest
, která vygeneruje HTTP POST požadavek. - Je nastaven MIME typ pro upload voláním metody
overrideMimeType()
. V našem případě je použit generický typ, stejný pro všechny soubory. Podle situace můžete chtít MIME typ nastavit, ale také nemusíte. - Nakonec je zavolána metoda
sendAsBinary()
, která pošle binární data ze souboru. (Tato část funguje pouze v prohlížečích s jádrem Gecko a měla by být změněna, protože používá pro načtení dat ze souboru synchronní rutinugetAsBinary()
, která je označena jako „deprecated“ a v jiných prohlížečích či v budoucích verzích nemusí být podporována.)
Škoda že není v HTML5 výběr víc souborů najednou.
K dokonalosti tomu chybí ještě další (dost podstatná) věc: aby mohla aplikace i soubory ukládat. Abych si mohl třeba načíst dokument do online editoru napsaném v JS, upravit ho a uložit, aniž by se něco odesílalo na server. Tohle prostě pokud vím v JS nejde nijak udělat a je to škoda.
Jde na něco takového použít Java, tam je ale zase fakt super jak pak vypadá „zabezpečení“ – povolit přístup na disk – ano/ne – ne „povolit přístup k souboru xyz“, případně „vybrat soubor k uložení“. U důvěryhodné aplikace to není problém, ale na používání jako se běžně používá JS na stránkách (tj. pustí se to samo, ani nedůvěryhodná aplikace mi nemůže ublížit) to není.
Bezpečným způsobem je to implementované ve Flashi – vytvoříte nějaký byte-array a vyvoláte funkci, která zobrazí uživateli dialog k uložení. No a díky dobře fungujícímu rozhraní Flash-JS si můžete udělat univerzální nevizuální flashovou komponentu k tomuto účelu… do doby, než to bude umět nativně HTML.
Tohle šlo s Java Applety už dávno ;-)
Je to tahle věc? http://java.sun.com/docs/books/tutorial/uiswing/components/filechooser.html
Tam je totiž právě problém, že nedávám aplikaci přístup k mnou vybranému souboru – vyskočí okno jestli chci povolit přístup a ani tam není napsané, jaký soubor aplikace chce – tzn. když dám Allow, může si vzít jakýkoliv.
Jenže tohle právě aplikaci dovolí pracovat jen se souborem, který uživatel vybral v dialogu, vyzkoušej si to.
a proč to neuložit na server a pak nestáhnout?
Hmm. Třeba protože je to neefektivní?
V čistém JavaScriptu to nejde, v JScriptu přes ActiveX ale ano.