Cách phát triển một trình tải lên tệp tương tác với JavaScript và Canvas
Ta có thể thực hiện các tương tác trên một trang web hoặc ứng dụng web tốt hay thú vị đến mức nào? Sự thật là hầu hết có thể tốt hơn ta ngày nay. Ví dụ, ai sẽ không muốn sử dụng một ứng dụng như thế này:
Tín dụng: Jakub Antalik khi rê bóng
Trong hướng dẫn này, ta sẽ thấy cách triển khai một thành phần sáng tạo để tải file lên, sử dụng làm nguồn cảm hứng cho hoạt ảnh trước đó của Jakub Antalík . Ý tưởng là mang lại phản hồi trực quan tốt hơn về những gì xảy ra với file sau khi bị xóa.
Ta sẽ chỉ tập trung vào việc triển khai các tương tác drag
và drop
và một số hoạt ảnh, mà không thực sự triển khai tất cả các logic cần thiết để thực sự tải file lên server và sử dụng thành phần trong production .
Đây là thành phần của ta sẽ trông như thế nào:
Bạn có thể xem bản demo trực tiếp hoặc chơi với mã trong Codepen . Nhưng nếu bạn cũng muốn biết nó hoạt động như thế nào, hãy tiếp tục đọc.
Trong phần hướng dẫn, ta sẽ thấy hai khía cạnh chính:
- Ta sẽ học cách triển khai một hệ thống hạt đơn giản bằng Javascript và Canvas.
- Ta sẽ triển khai mọi thứ cần thiết để xử lý các sự kiện
drag
vàdrop
.
Ngoài các công nghệ thông thường (HTML, CSS, Javascript), để viết mã thành phần của ta , ta sẽ sử dụng thư viện hoạt hình nhẹ anime.js .
Bước 1 - Tạo cấu trúc HTML
Trong trường hợp này, cấu trúc HTML của ta sẽ khá cơ bản:
<!-- Form to upload the files --> <form class="upload" method="post" action="" enctype="multipart/form-data" novalidate=""> <!-- The `input` of type `file` --> <input class="upload__input" name="files[]" type="file" multiple=""/> <!-- The `canvas` element to draw the particles --> <canvas class="upload__canvas"></canvas> <!-- The upload icon --> <div class="upload__icon"><svg viewBox="0 0 470 470"><path d="m158.7 177.15 62.8-62.8v273.9c0 7.5 6 13.5 13.5 13.5s13.5-6 13.5-13.5v-273.9l62.8 62.8c2.6 2.6 6.1 4 9.5 4 3.5 0 6.9-1.3 9.5-4 5.3-5.3 5.3-13.8 0-19.1l-85.8-85.8c-2.5-2.5-6-4-9.5-4-3.6 0-7 1.4-9.5 4l-85.8 85.8c-5.3 5.3-5.3 13.8 0 19.1 5.2 5.2 13.8 5.2 19 0z"></path></svg></div> </form>
Như bạn thấy , ta chỉ cần một phần tử form
và một input
loại file
để cho phép tải file lên server . Trong thành phần của ta , ta cũng cần một phần tử canvas
để vẽ các hạt và biểu tượng SVG.
Lưu ý để sử dụng một thành phần như thế này trong production , bạn phải điền thuộc tính action
vào biểu mẫu và có thể thêm một phần tử label
cho đầu vào, v.v.
Bước 2 - Thêm kiểu CSS
Ta sẽ sử dụng SCSS làm bộ tiền xử lý CSS, nhưng các kiểu ta đang sử dụng rất gần với CSS thuần túy và chúng khá đơn giản.
Hãy bắt đầu bằng cách định vị các phần tử form
và canvas
, trong số các kiểu cơ bản khác:
// Position `form` and `canvas` full width and height .upload, .upload__canvas { position: absolute; left: 0; top: 0; width: 100%; height: 100%; } // Position the `canvas` behind all other elements .upload__canvas { z-index: -1; } // Hide the file `input` .upload__input { display: none; }
Bây giờ ta hãy xem các kiểu cần thiết cho form
của ta , cho cả trạng thái ban đầu (ẩn) và khi nó đang hoạt động ( user đang kéo file để tải lên). Mã đã được comment đầy đủ để hiểu rõ hơn:
// Styles for the upload `form` .upload { z-index: 1; // should be the higher `z-index` // Styles for the `background` background-color: rgba(4, 72, 59, 0.8); background-image: radial-gradient(ellipse at 50% 120%, rgba(4, 72, 59, 1) 10%, rgba(4, 72, 59, 0) 40%); background-position: 0 300px; background-repeat: no-repeat; // Hide it by default opacity: 0; visibility: hidden; // Transition transition: 0.5s; // Upload overlay, that prevent the event `drag-leave` to be triggered while dragging over inner elements &:after { position: absolute; content: ''; left: 0; top: 0; width: 100%; height: 100%; } } // Styles applied while files are being dragging over the screen .upload--active { // Translate the `radial-gradient` background-position: 0 0; // Show the upload component opacity: 1; visibility: visible; // Only transition `opacity`, preventing issues with `visibility` transition-property: opacity; }
Cuối cùng, hãy xem các kiểu đơn giản mà ta đã áp dụng cho biểu tượng tải lên:
// Styles for the icon .upload__icon { position: relative; left: calc(50% - 40px); top: calc(50% - 40px); width: 80px; height: 80px; padding: 15px; border-radius: 100%; background-color: #EBF2EA; path { fill: rgba(4, 72, 59, 0.8); } }
Bây giờ thành phần của ta trông giống như ta muốn, vì vậy ta đã sẵn sàng thêm tính tương tác với Javascript.
Bước 3 - Phát triển một hệ thống hạt
Trước khi triển khai chức năng drag
và drop
, hãy xem cách ta có thể triển khai một hệ thống hạt.
Trong hệ thống hạt của ta , mỗi hạt sẽ là một Object
Javascript đơn giản với các tham số cơ bản để xác định cách hạt hoạt động. Và tất cả các hạt sẽ được lưu trữ trong một Array
, trong đó mã của ta được gọi là particles
.
Sau đó, thêm một hạt mới vào hệ thống của ta là việc tạo một Object
Javascrit mới và thêm nó vào mảng particles
. Kiểm tra các comment để bạn hiểu mục đích của từng thuộc tính:
// Create a new particle function createParticle(options) { var o = options || {}; particles.push({ 'x': o.x, // particle position in the `x` axis 'y': o.y, // particle position in the `y` axis 'vx': o.vx, // in every update (animation frame) the particle will be translated this amount of pixels in `x` axis 'vy': o.vy, // in every update (animation frame) the particle will be translated this amount of pixels in `y` axis 'life': 0, // in every update (animation frame) the life will increase 'death': o.death || Math.random() * 200, // consider the particle dead when the `life` reach this value 'size': o.size || Math.floor((Math.random() * 2) + 1) // size of the particle }); }
Bây giờ ta đã xác định cấu trúc cơ bản của hệ thống hạt của bạn , ta cần một hàm vòng lặp, cho phép ta thêm các hạt mới, cập nhật chúng và vẽ chúng trên canvas
trong mỗi khung hoạt hình. Thông tin như thế này:
// Loop to redraw the particles on every frame function loop() { addIconParticles(); // add new particles for the upload icon updateParticles(); // update all particles renderParticles(); // clear `canvas` and draw all particles iconAnimationFrame = requestAnimationFrame(loop); // loop }
Bây giờ ta hãy xem cách ta đã định nghĩa tất cả các hàm mà ta gọi bên trong vòng lặp. Như mọi khi, hãy chú ý đến các comment :
// Add new particles for the upload icon function addIconParticles() { iconRect = uploadIcon.getBoundingClientRect(); // get icon dimensions var i = iconParticlesCount; // how many particles we should add? while (i--) { // Add a new particle createParticle({ x: iconRect.left + iconRect.width / 2 + rand(iconRect.width - 10), // position the particle along the icon width in the `x` axis y: iconRect.top + iconRect.height / 2, // position the particle centered in the `y` axis vx: 0, // the particle will not be moved in the `x` axis vy: Math.random() * 2 * iconParticlesCount // value to move the particle in the `y` axis, greater is faster }); } } // Update the particles, removing the dead ones function updateParticles() { for (var i = 0; i < particles.length; i++) { if (particles[i].life > particles[i].death) { particles.splice(i, 1); } else { particles[i].x += particles[i].vx; particles[i].y += particles[i].vy; particles[i].life++; } } } // Clear the `canvas` and redraw every particle (rect) function renderParticles() { ctx.clearRect(0, 0, canvasWidth, canvasHeight); for (var i = 0; i < particles.length; i++) { ctx.fillStyle = 'rgba(255, 255, 255, ' + (1 - particles[i].life / particles[i].death) + ')'; ctx.fillRect(particles[i].x, particles[i].y, particles[i].size, particles[i].size); } }
Và ta đã sẵn sàng hệ thống hạt của bạn , nơi ta có thể thêm các hạt mới xác định các tùy chọn ta muốn và vòng lặp sẽ chịu trách nhiệm thực hiện hoạt ảnh.
Thêm hoạt ảnh cho biểu tượng tải lên
Bây giờ, hãy xem cách ta chuẩn bị cho biểu tượng tải lên thành hoạt ảnh:
// Add 100 particles for the icon (without render), so the animation will not look empty at first function initIconParticles() { var iconParticlesInitialLoop = 100; while (iconParticlesInitialLoop--) { addIconParticles(); updateParticles(); } } initIconParticles(); // Alternating animation for the icon to translate in the `y` axis function initIconAnimation() { iconAnimation = anime({ targets: uploadIcon, translateY: -10, duration: 800, easing: 'easeInOutQuad', direction: 'alternate', loop: true, autoplay: false // don't execute the animation yet, only on `drag` events (see later) }); } initIconAnimation();
Với mã trước đó, ta chỉ cần một số chức năng khác để tạm dừng hoặc tiếp tục hoạt ảnh của biểu tượng tải lên, nếu thích hợp:
// Play the icon animation (`translateY` and particles) function playIconAnimation() { if (!playingIconAnimation) { playingIconAnimation = true; iconAnimation.play(); iconAnimationFrame = requestAnimationFrame(loop); } } // Pause the icon animation (`translateY` and particles) function pauseIconAnimation() { if (playingIconAnimation) { playingIconAnimation = false; iconAnimation.pause(); cancelAnimationFrame(iconAnimationFrame); } }
Bước 4 - Thêm chức năng kéo và thả
Sau đó, ta có thể bắt đầu thêm chức năng drag
và drop
để tải file lên. Hãy bắt đầu bằng cách ngăn chặn các hành vi không mong muốn cho mỗi sự kiện liên quan:
// Preventing the unwanted behaviours ['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave', 'drop'].forEach(function (event) { document.addEventListener(event, function (e) { e.preventDefault(); e.stopPropagation(); }); });
Bây giờ ta sẽ xử lý các sự kiện kiểu drag
, nơi ta sẽ kích hoạt form
để nó được hiển thị và ta sẽ phát các hình ảnh động cho biểu tượng tải lên:
// Show the upload component on `dragover` and `dragenter` events ['dragover', 'dragenter'].forEach(function (event) { document.addEventListener(event, function () { if (!animatingUpload) { uploadForm.classList.add('upload--active'); playIconAnimation(); } }); });
Trong trường hợp user rời khỏi khu vực drop
, ta chỉ cần ẩn form
và tạm dừng các hoạt ảnh cho biểu tượng tải lên:
// Hide the upload component on `dragleave` and `dragend` events ['dragleave', 'dragend'].forEach(function (event) { document.addEventListener(event, function () { if (!animatingUpload) { uploadForm.classList.remove('upload--active'); pauseIconAnimation(); } }); });
Và cuối cùng sự kiện quan trọng nhất mà ta phải xử lý là sự kiện drop
, bởi vì đó sẽ là nơi ta lấy các file mà user đã drop, ta sẽ thực thi các hoạt ảnh tương ứng và nếu đây là một thành phần đầy đủ chức năng, ta sẽ tải lên file tới server thông qua AJAX.
// Handle the `drop` event document.addEventListener('drop', function (e) { if (!animatingUpload) { // If no animation in progress droppedFiles = e.dataTransfer.files; // the files that were dropped filesCount = droppedFiles.length > 3 ? 3 : droppedFiles.length; // the number of files (1-3) to perform the animations if (filesCount) { animatingUpload = true; // Add particles for every file loaded (max 3), also staggered (increasing delay) var i = filesCount; while (i--) { addParticlesOnDrop(e.pageX + (i ? rand(100) : 0), e.pageY + (i ? rand(100) : 0), 200 * i); } // Hide the upload component after the animation setTimeout(function () { uploadForm.classList.remove('upload--active'); }, 1500 + filesCount * 150); // Here is the right place to call something like: // triggerFormSubmit(); // A function to actually upload the files to the server } else { // If no files where dropped, just hide the upload component uploadForm.classList.remove('upload--active'); pauseIconAnimation(); } } });
Trong đoạn mã trước, ta đã thấy rằng hàm addParticlesOnDrop
được gọi, có nhiệm vụ thực thi hoạt ảnh hạt từ nơi các file được thả xuống. Hãy xem cách ta có thể triển khai chức năng này:
// Create a new particles on `drop` event function addParticlesOnDrop(x, y, delay) { // Add a few particles when the `drop` event is triggered var i = delay ? 0 : 20; // Only add extra particles for the first item dropped (no `delay`) while (i--) { createParticle({ x: x + rand(30), y: y + rand(30), vx: rand(2), vy: rand(2), death: 60 }); } // Now add particles along the way where the user `drop` the files to the icon position // Learn more about this kind of animation in the `anime.js` documentation anime({ targets: {x: x, y: y}, x: iconRect.left + iconRect.width / 2, y: iconRect.top + iconRect.height / 2, duration: 500, delay: delay || 0, easing: 'easeInQuad', run: function (anim) { var target = anim.animatables[0].target; var i = 10; while (i--) { createParticle({ x: target.x + rand(30), y: target.y + rand(30), vx: rand(2), vy: rand(2), death: 60 }); } }, complete: uploadIconAnimation // call the second part of the animation }); }
Cuối cùng, khi các phần tử đến vị trí của biểu tượng, ta phải di chuyển biểu tượng lên trên, tạo cảm giác rằng các file đang được tải lên:
// Translate and scale the upload icon function uploadIconAnimation() { iconParticlesCount += 2; // add more particles per frame, to get a speed up feeling anime.remove(uploadIcon); // stop current animations // Animate the icon using `translateY` and `scale` iconAnimation = anime({ targets: uploadIcon, translateY: { value: -canvasHeight / 2 - iconRect.height, duration: 1000, easing: 'easeInBack' }, scale: { value: '+=0.1', duration: 2000, elasticity: 800 }, complete: function () { // reset the icon and all animation variables to its initial state setTimeout(resetAll, 0); } }); }
Để kết thúc, ta phải triển khai hàm resetAll
, hàm này sẽ đặt lại biểu tượng và tất cả các biến về trạng thái ban đầu. Ta cũng phải cập nhật kích thước canvas
và đặt lại thành phần trên sự kiện resize
. Nhưng để không thực hiện hướng dẫn này lâu hơn nữa, ta đã không bao gồm những điều này và các chi tiết nhỏ khác, mặc dù bạn có thể kiểm tra mã hoàn chỉnh trong kho lưu trữ Github .
Kết luận
Và cuối cùng thành phần của ta đã hoàn thành! Hãy cùng xem:
Bạn có thể kiểm tra bản demo trực tiếp , chơi với mã trên Codepen hoặc nhận mã đầy đủ trên Github .
Trong suốt hướng dẫn, ta đã thấy cách tạo một hệ thống hạt đơn giản, cũng như xử lý các sự kiện drag
và drop
để triển khai thành phần tải lên file bắt mắt.
Lưu ý thành phần này chưa sẵn sàng để sử dụng trong production . Trong trường hợp bạn muốn hoàn thành việc triển khai để làm cho nó hoạt động đầy đủ, tôi khuyên bạn nên xem hướng dẫn tuyệt vời này trong CSS Tricks .
Các tin liên quan
Cách gói một gói JavaScript Vanilla để sử dụng trong React2019-12-12
Cách sử dụng map (), filter () và Reduce () trong JavaScript
2019-12-12
Giải thích về lập trình chức năng JavaScript: Ứng dụng một phần và làm xoăn
2019-12-12
Giới thiệu về Closures và Currying trong JavaScript
2019-12-12
Bắt đầu với các hàm mũi tên ES6 trong JavaScript
2019-12-12
Cách đếm số nguyên âm trong một chuỗi văn bản bằng thuật toán JavaScript
2019-12-12
Cách sử dụng phép gán cấu trúc hủy trong JavaScript
2019-12-12
Giải thích về lập trình chức năng JavaScript: Kết hợp & truyền tải
2019-12-12
Cách sử dụng .every () và .some () để điều khiển mảng JavaScript
2019-12-12
Chuyển đổi Mảng sang Chuỗi trong JavaScript
2019-12-05