Jak zrobić program do przesyłania plików typu „przeciągnij i upuść” za pomocą waniliowego JavaScript

Opublikowany: 2022-03-10
Szybkie podsumowanie ↬ W tym artykule do ukończenia tego projektu użyjemy „waniliowego” JavaScript ES2015+ (bez frameworków i bibliotek) i zakładamy, że masz praktyczną wiedzę na temat JavaScript w przeglądarce. Ten przykład powinien być kompatybilny z każdą wiecznie zieloną przeglądarką oraz IE 10 i 11.

Wiadomo, że dane wejściowe wyboru plików są trudne do stylizacji tak, jak chcą programiści, więc wielu po prostu je ukrywa i tworzy przycisk, który zamiast tego otwiera okno dialogowe wyboru pliku. Obecnie mamy jednak jeszcze bardziej wyszukany sposób obsługi wyboru plików: przeciągnij i upuść.

Z technicznego punktu widzenia było to już możliwe, ponieważ większość (jeśli nie wszystkie ) implementacji wejścia wyboru pliku umożliwiało przeciąganie plików nad nim, aby je wybrać, ale wymaga to faktycznego wyświetlenia elementu file . Tak więc użyjmy interfejsów API udostępnionych nam przez przeglądarkę, aby zaimplementować selektor i narzędzie do przesyłania plików metodą „przeciągnij i upuść”.

W tym artykule do ukończenia tego projektu użyjemy „waniliowego” JavaScript ES2015+ (bez frameworków i bibliotek) i zakładamy, że masz praktyczną wiedzę na temat JavaScript w przeglądarce. Ten przykład — poza składnią ES2015+, którą można łatwo zmienić na składnię ES5 lub transpilować przez Babel — powinien być kompatybilny z każdą wiecznie zieloną przeglądarką oraz IE 10 i 11.

Oto krótkie spojrzenie na to, co będziesz robić:

Narzędzie do przesyłania obrazów metodą „przeciągnij i upuść” w akcji
Demonstracja strony internetowej, na której można przesyłać obrazy metodą „przeciągnij i upuść”, natychmiast wyświetlić podgląd przesyłanych obrazów i zobaczyć postęp przesyłania na pasku postępu.

Wydarzenia typu „przeciągnij i upuść”

Pierwszą rzeczą, którą musimy omówić, są wydarzenia związane z przeciąganiem i upuszczaniem, ponieważ są one siłą napędową tej funkcji. W sumie istnieje osiem zdarzeń, które uruchamia przeglądarka, związanych z przeciąganiem i upuszczaniem: drag , dragend , dragenter , dragexit , dragleave , dragover , dragstart i drop . Nie będziemy omawiać ich wszystkich, ponieważ drag , dragend , dragexit i dragstart są uruchamiane na przeciąganym elemencie, a w naszym przypadku będziemy przeciągać pliki z naszego systemu plików, a nie elementy DOM , więc te wydarzenia nigdy się nie pojawią.

Jeśli jesteś ich ciekaw, możesz przeczytać dokumentację dotyczącą tych wydarzeń na MDN.

Więcej po skoku! Kontynuuj czytanie poniżej ↓

Jak można się spodziewać, możesz zarejestrować programy obsługi zdarzeń dla tych zdarzeń w ten sam sposób, w jaki rejestrujesz programy obsługi zdarzeń dla większości zdarzeń przeglądarki: poprzez addEventListener .

 let dropArea = document.getElementById('drop-area') dropArea.addEventListener('dragenter', handlerFunction, false) dropArea.addEventListener('dragleave', handlerFunction, false) dropArea.addEventListener('dragover', handlerFunction, false) dropArea.addEventListener('drop', handlerFunction, false)

Oto mała tabela opisująca, co robią te zdarzenia, używając dropArea z przykładowego kodu, aby język był bardziej przejrzysty:

Wydarzenie Kiedy zostanie zwolniony?
dragenter Przeciągany element jest przeciągany nad dropArea, co czyni go celem zdarzenia drop, jeśli użytkownik go tam upuści.
dragleave Przeciągany element jest przeciągany z dropArea na inny element, co czyni go celem zdarzenia drop.
dragover Co kilkaset milisekund, gdy przeciągany element znajduje się nad dropArea i jest w ruchu.
drop Użytkownik zwalnia przycisk myszy, upuszczając przeciągnięty element na dropArea.

Zauważ, że przeciągany element jest przeciągany nad element potomny dropArea , dragleave uruchomi się na dropArea , a dragenter uruchomi się na tym elemencie potomnym, ponieważ jest to nowy target . Zdarzenie drop będzie propagowane do dropArea (chyba że propagacja zostanie zatrzymana przez inny detektor zdarzeń, zanim tam dotrze), więc nadal będzie uruchamiana na dropArea , mimo że nie jest target zdarzenia.

Pamiętaj też, że aby utworzyć niestandardowe interakcje typu „przeciągnij i upuść”, musisz wywołać event.preventDefault() w każdym z detektorów tych zdarzeń. Jeśli tego nie zrobisz, przeglądarka otworzy upuszczony plik zamiast wysyłać go do modułu obsługi zdarzenia drop .

Konfiguracja naszego formularza

Zanim zaczniemy dodawać funkcję przeciągania i upuszczania, będziemy potrzebować podstawowego formularza ze standardowym wejściem file . Technicznie nie jest to konieczne, ale dobrym pomysłem jest zapewnienie go jako alternatywy na wypadek, gdyby użytkownik miał przeglądarkę bez obsługi interfejsu API przeciągania i upuszczania.

 <div> <form class="my-form"> <p>Upload multiple files with the file dialog or by dragging and dropping images onto the dashed region</p> <input type="file" multiple accept="image/*" onchange="handleFiles(this.files)"> <label class="button" for="fileElem">Select some files</label> </form> </div>

Dość prosta konstrukcja. Możesz zauważyć obsługę onchange na input . Przyjrzymy się temu później. Dobrym pomysłem byłoby również dodanie action do form i przycisku submit , aby pomóc osobom, które nie mają włączonej obsługi JavaScript. Następnie możesz użyć JavaScript, aby się ich pozbyć, aby uzyskać czystszy formularz. W każdym razie będziesz potrzebować skryptu po stronie serwera, aby zaakceptować przesyłanie, niezależnie od tego, czy jest to coś opracowanego wewnętrznie, czy korzystasz z usługi takiej jak Cloudinary, aby zrobić to za Ciebie. Poza tymi notatkami nie ma tu nic specjalnego, więc wrzućmy kilka stylów:

 #drop-area { border: 2px dashed #ccc; border-radius: 20px; width: 480px; font-family: sans-serif; margin: 100px auto; padding: 20px; } #drop-area.highlight { border-color: purple; } p { margin-top: 0; } .my-form { margin-bottom: 10px; } #gallery { margin-top: 10px; } #gallery img { width: 150px; margin-bottom: 10px; margin-right: 10px; vertical-align: middle; } .button { display: inline-block; padding: 10px; background: #ccc; cursor: pointer; border-radius: 5px; border: 1px solid #ccc; } .button:hover { background: #ddd; } #fileElem { display: none; }

Wiele z tych stylów jeszcze nie wchodzi w grę, ale to jest w porządku. Najważniejsze na razie jest to, że wejście file jest ukryte, ale jego label jest stylizowana na przycisk, więc ludzie zdadzą sobie sprawę, że mogą go kliknąć, aby wyświetlić okno dialogowe wyboru pliku. Podążamy również za konwencją, obrysowując obszar upuszczania liniami przerywanymi.

Dodawanie funkcji przeciągania i upuszczania

Teraz dochodzimy do sedna sytuacji: przeciągnij i upuść. Wrzućmy skrypt na dole strony lub w osobnym pliku, jakkolwiek masz na to ochotę. Pierwszą rzeczą, jakiej potrzebujemy w skrypcie, jest odniesienie do obszaru upuszczania, dzięki czemu możemy dołączyć do niego kilka zdarzeń:

 let dropArea = document.getElementById('drop-area')

Dodajmy teraz kilka wydarzeń. Zaczniemy od dodania programów obsługi do wszystkich zdarzeń, aby zapobiec domyślnym zachowaniom i aby zdarzenia nie bulgotały wyżej niż to konieczne:

 ;['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { dropArea.addEventListener(eventName, preventDefaults, false) }) function preventDefaults (e) { e.preventDefault() e.stopPropagation() }

Teraz dodajmy wskaźnik, aby użytkownik wiedział, że rzeczywiście przeciągnął element przez właściwy obszar, używając CSS do zmiany koloru obramowania obszaru upuszczania. Style powinny już znajdować się pod selektorem #drop-area.highlight , więc użyjmy JS, aby dodać i usunąć tę klasę highlight , gdy jest to konieczne.

 ;['dragenter', 'dragover'].forEach(eventName => { dropArea.addEventListener(eventName, highlight, false) }) ;['dragleave', 'drop'].forEach(eventName => { dropArea.addEventListener(eventName, unhighlight, false) }) function highlight(e) { dropArea.classList.add('highlight') } function unhighlight(e) { dropArea.classList.remove('highlight') }

Do podświetlania musieliśmy użyć zarówno dragenter , jak i dragover , z powodu tego, o czym wspomniałem wcześniej. Jeśli zaczniesz najeżdżać bezpośrednio na dropArea , a następnie na jednym z jego elementów podrzędnych, dragleave zostanie uruchomiony, a podświetlenie zostanie usunięte. Zdarzenie dragover jest uruchamiane po zdarzeniach dragenter i dragleave , więc podświetlenie zostanie ponownie dodane do dropArea , zanim zobaczymy, że zostało usunięte.

Usuwamy również podświetlenie, gdy przeciągany element opuszcza wyznaczony obszar lub po upuszczeniu elementu.

Teraz wszystko, co musimy zrobić, to dowiedzieć się, co zrobić, gdy niektóre pliki zostaną upuszczone:

 dropArea.addEventListener('drop', handleDrop, false) function handleDrop(e) { let dt = e.dataTransfer let files = dt.files handleFiles(files) }

Nie zbliża nas to do końca, ale robi dwie ważne rzeczy:

  1. Pokazuje, jak uzyskać dane dla plików, które zostały usunięte.
  2. Przenosi nas do tego samego miejsca, w którym znajdowało się input file z jego obsługą onchange : czeka na handleFiles .

Pamiętaj, że files nie są tablicą, ale FileList . Tak więc, kiedy zaimplementujemy handleFiles , będziemy musieli przekonwertować go na tablicę, aby łatwiej go iterować:

 function handleFiles(files) { ([...files]).forEach(uploadFile) }

To było antyklimatyczne. Przejdźmy do uploadFile dla prawdziwych mięsistych rzeczy.

 function uploadFile(file) { let url = 'YOUR URL HERE' let formData = new FormData() formData.append('file', file) fetch(url, { method: 'POST', body: formData }) .then(() => { /* Done. Inform the user */ }) .catch(() => { /* Error. Inform the user */ }) }

Tutaj używamy FormData , wbudowanego interfejsu API przeglądarki do tworzenia danych formularza do wysłania na serwer. Następnie używamy interfejsu API fetch , aby faktycznie wysłać obraz na serwer. Upewnij się, że zmieniłeś adres URL, aby działał z Twoim zapleczem lub usługą, i formData.append dodatkowe dane formularza, które mogą być potrzebne, aby zapewnić serwerowi wszystkie potrzebne informacje. Alternatywnie, jeśli chcesz obsługiwać Internet Explorer, możesz użyć XMLHttpRequest , co oznacza, że uploadFile będzie wyglądał tak:

 function uploadFile(file) { var url = 'YOUR URL HERE' var xhr = new XMLHttpRequest() var formData = new FormData() xhr.open('POST', url, true) xhr.addEventListener('readystatechange', function(e) { if (xhr.readyState == 4 && xhr.status == 200) { // Done. Inform the user } else if (xhr.readyState == 4 && xhr.status != 200) { // Error. Inform the user } }) formData.append('file', file) xhr.send(formData) }

W zależności od konfiguracji serwera możesz chcieć sprawdzić różne zakresy numerów status , a nie tylko 200 , ale dla naszych celów to zadziała.

Dodatkowe funkcje

To cała podstawowa funkcjonalność, ale często chcemy więcej funkcjonalności. W szczególności w tym samouczku dodamy panel podglądu, który wyświetla użytkownikowi wszystkie wybrane obrazy, a następnie dodamy pasek postępu, który pozwoli użytkownikowi zobaczyć postęp przesyłania. Zacznijmy więc od podglądu obrazów.

Podgląd obrazu

Można to zrobić na kilka sposobów: możesz poczekać, aż obraz zostanie przesłany i poprosić serwer o przesłanie adresu URL obrazu, ale oznacza to, że musisz poczekać, a obrazy mogą czasami być dość duże. Alternatywą — którą omówimy dzisiaj — jest użycie interfejsu API FileReader na danych pliku, które otrzymaliśmy ze zdarzenia drop . Jest to asynchroniczne i możesz alternatywnie użyć FileReaderSync, ale możemy próbować odczytać kilka dużych plików z rzędu, więc może to zablokować wątek na jakiś czas i naprawdę zrujnować wrażenia. Stwórzmy więc funkcję previewFile i zobaczmy, jak to działa:

 function previewFile(file) { let reader = new FileReader() reader.readAsDataURL(file) reader.onloadend = function() { let img = document.createElement('img') img.src = reader.result document.getElementById('gallery').appendChild(img) } }

Tutaj tworzymy new FileReader i wywołujemy na nim readAsDataURL za pomocą obiektu File . Jak wspomniano, jest to asynchroniczne, więc musimy dodać obsługę zdarzeń onloadend , aby uzyskać wynik odczytu. Następnie używamy podstawowego adresu URL 64 danych jako src nowego elementu obrazu i dodajemy go do elementu gallery . Są tylko dwie rzeczy, które trzeba zrobić, aby to zadziałało teraz: dodać element gallery i upewnić się, że previewFile jest rzeczywiście wywołany.

Najpierw dodaj następujący kod HTML zaraz po końcu tagu form :

 <div></div>

Nic specjalnego; to tylko div. Style są już dla niego określone i zawarte w nim obrazy, więc nie ma już nic do zrobienia. Zmieńmy teraz funkcję handleFiles na następującą:

 function handleFiles(files) { files = [...files] files.forEach(uploadFile) files.forEach(previewFile) }

Można to zrobić na kilka sposobów, takich jak kompozycja lub pojedyncze wywołanie zwrotne do forEach , które uruchomiło w nim uploadFile i previewFile , ale to też działa. Dzięki temu po upuszczeniu lub wybraniu niektórych obrazów powinny one pojawić się niemal natychmiast pod formularzem. Interesujące jest to, że — w niektórych aplikacjach — możesz nie chcieć przesyłać obrazów, ale zamiast tego przechowuj ich adresy URL danych w localStorage lub innej pamięci podręcznej po stronie klienta, aby aplikacja miała do nich dostęp później. Osobiście nie mogę wymyślić żadnych dobrych przypadków użycia, ale mogę się założyć, że są takie.

Śledzenie postępu

Jeśli coś może zająć trochę czasu, pasek postępu może pomóc użytkownikowi zorientować się, że postęp jest rzeczywiście dokonywany i wskazać, ile czasu zajmie jego ukończenie. Dodanie wskaźnika postępu jest dość łatwe dzięki tagowi progress HTML5. Zacznijmy od dodania tego do kodu HTML tym razem.

 <progress max=100 value=0></progress>

Możesz umieścić go tuż za label lub między form a div galerii, w zależności od tego, co wolisz. Jeśli o to chodzi, możesz umieścić go w dowolnym miejscu w tagach body . Do tego przykładu nie dodano żadnych stylów, więc pokaże on domyślną implementację przeglądarki, która jest obsługiwana. Teraz popracujmy nad dodaniem JavaScript. Najpierw przyjrzymy się implementacji za pomocą fetch , a następnie pokażemy wersję dla XMLHttpRequest . Na początek potrzebujemy kilku nowych zmiennych na górze skryptu:

 let filesDone = 0 let filesToDo = 0 let progressBar = document.getElementById('progress-bar')

Podczas korzystania z funkcji fetch jesteśmy w stanie określić tylko, kiedy przesyłanie zostało zakończone, więc jedyne informacje, które śledzimy, to liczba plików wybranych do przesłania (jako filesToDo ) i liczba plików, które zostały zakończone (jako filesDone ). Zachowujemy również odniesienie do elementu #progress-bar , aby móc go szybko zaktualizować. Teraz stwórzmy kilka funkcji do zarządzania postępem:

 function initializeProgress(numfiles) { progressBar.value = 0 filesDone = 0 filesToDo = numfiles } function progressDone() { filesDone++ progressBar.value = filesDone / filesToDo * 100 }

Gdy zaczniemy przesyłanie, initializeProgress w celu zresetowania paska postępu. Następnie przy każdym zakończonym przesyłaniu wywołamy progressDone , aby zwiększyć liczbę ukończonych operacji przesyłania i zaktualizować pasek postępu, aby pokazać bieżący postęp. Wywołajmy więc te funkcje, aktualizując kilka starych funkcji:

 function handleFiles(files) { files = [...files] initializeProgress(files.length) // <- Add this line files.forEach(uploadFile) files.forEach(previewFile) } function uploadFile(file) { let url = 'YOUR URL HERE' let formData = new FormData() formData.append('file', file) fetch(url, { method: 'POST', body: formData }) .then(progressDone) // <- Add `progressDone` call here .catch(() => { /* Error. Inform the user */ }) }

I to wszystko. Przyjrzyjmy się teraz implementacji XMLHttpRequest . Moglibyśmy po prostu zrobić szybką aktualizację do uploadFile , ale XMLHttpRequest w rzeczywistości daje nam więcej funkcji niż fetch , a mianowicie jesteśmy w stanie dodać detektor zdarzeń dla postępu wysyłania do każdego żądania, który okresowo będzie dostarczał nam informacji o tym, jaka część żądania jest skończone. Z tego powodu musimy śledzić procent wykonania każdego żądania, a nie tylko liczbę wykonanych. Zacznijmy więc od zastąpienia deklaracji filesDone i filesToDo następującymi:

 let uploadProgress = []

Następnie musimy zaktualizować również nasze funkcje. Zmienimy nazwę progressDone , aby updateProgress i zmienimy je na następujące:

 function initializeProgress(numFiles) { progressBar.value = 0 uploadProgress = [] for(let i = numFiles; i > 0; i--) { uploadProgress.push(0) } } function updateProgress(fileNumber, percent) { uploadProgress[fileNumber] = percent let total = uploadProgress.reduce((tot, curr) => tot + curr, 0) / uploadProgress.length progressBar.value = total }

Teraz initializeProgress inicjuje tablicę o długości równej numFiles , która jest wypełniona zerami, co oznacza, że ​​każdy plik jest kompletny w 0%. W updateProgress dowiadujemy się, który obraz jest aktualizowany i zmieniamy wartość tego indeksu na podany percent . Następnie obliczamy całkowity procent postępu, biorąc średnią wszystkich wartości procentowych i aktualizujemy pasek postępu, aby odzwierciedlał obliczoną sumę. Nadal wywołujemy initializeProgress w handleFiles tak samo, jak w przykładzie fetch , więc teraz wszystko, co musimy zaktualizować, to uploadFile , aby wywołać updateProgress .

 function uploadFile(file, i) { // <- Add `i` parameter var url = 'YOUR URL HERE' var xhr = new XMLHttpRequest() var formData = new FormData() xhr.open('POST', url, true) // Add following event listener xhr.upload.addEventListener("progress", function(e) { updateProgress(i, (e.loaded * 100.0 / e.total) || 100) }) xhr.addEventListener('readystatechange', function(e) { if (xhr.readyState == 4 && xhr.status == 200) { // Done. Inform the user } else if (xhr.readyState == 4 && xhr.status != 200) { // Error. Inform the user } }) formData.append('file', file) xhr.send(formData) }

Pierwszą rzeczą, na którą należy zwrócić uwagę, jest to, że dodaliśmy parametr i . To jest indeks pliku na liście plików. Nie musimy aktualizować handleFiles , aby przekazać ten parametr, ponieważ używa on forEach , który już podaje indeks elementu jako drugi parametr wywołań zwrotnych. Dodaliśmy również detektor zdarzeń progress do xhr.upload , dzięki czemu możemy wywoływać updateProgress wraz z postępem. Obiekt zdarzenia (oznaczany jako e w kodzie) zawiera dwie istotne informacje: load, który zawiera liczbę loaded do tej pory bajtów oraz total , który zawiera całkowitą liczbę bajtów pliku.

|| 100 Kawałek || 100 jest tam, ponieważ czasami, jeśli wystąpi błąd, e.loaded i e.total będą wynosić zero, co oznacza, że ​​obliczenia wyjdą jako NaN , więc 100 jest używane zamiast tego, aby zgłosić, że plik jest gotowy. Możesz również użyć 0 . W obu przypadkach błąd pojawi się w module obsługi readystatechange , abyś mógł poinformować o nim użytkownika. Ma to na celu jedynie zapobieganie rzucaniu wyjątków za próby matematyczne z NaN .

Wniosek

To ostatni kawałek. Masz teraz stronę internetową, na której możesz przesyłać obrazy za pomocą przeciągania i upuszczania, natychmiast podglądać przesyłane obrazy i sprawdzać postęp przesyłania na pasku postępu. Możesz zobaczyć ostateczną wersję (z XMLHttpRequest ) w akcji na CodePen, ale pamiętaj, że usługa, do której przesyłam pliki, ma ograniczenia, więc jeśli wiele osób ją przetestuje, może się na jakiś czas zepsuć.