Cách tạo trình tải tệp kéo và thả bằng Vue.js 3

theanh

Administrator
Nhân viên
Điều gì khác biệt giữa trình tải tệp mà chúng ta đang xây dựng trong bài viết này so với trình tải trước? Trình tải tệp kéo và thả trước đó được xây dựng bằng Vanilla JS và thực sự tập trung vào cách thực hiện tải tệp và chọn tệp kéo và thả, do đó, bộ tính năng của nó bị hạn chế. Nó tải tệp ngay sau khi bạn chọn chúng bằng thanh tiến trình đơn giản và bản xem trước hình thu nhỏ của hình ảnh. Bạn có thể xem tất cả những điều này tại bản demo này.

Ngoài việc sử dụng Vue, chúng tôi sẽ thay đổi các tính năng: sau khi thêm hình ảnh, hình ảnh sẽ không được tải lên ngay lập tức. Thay vào đó, bản xem trước hình thu nhỏ sẽ hiển thị. Sẽ có một nút ở góc trên bên phải của hình thu nhỏ để xóa tệp khỏi danh sách trong trường hợp bạn không cố ý chọn ảnh hoặc thay đổi ý định về việc tải ảnh lên.

Sau đó, bạn sẽ nhấp vào nút "Tải lên" để gửi dữ liệu ảnh đến máy chủ và mỗi ảnh sẽ hiển thị trạng thái tải lên của ảnh đó. Để hoàn thiện hơn, tôi đã tạo ra một số kiểu đẹp mắt (mặc dù tôi không phải là nhà thiết kế, vì vậy đừng đánh giá quá khắt khe). Chúng ta sẽ không đào sâu vào những kiểu đó trong hướng dẫn này, nhưng bạn sẽ có thể sao chép hoặc tự mình sàng lọc chúng trong Kho lưu trữ GitHub — tuy nhiên, nếu bạn định sao chép chúng, hãy đảm bảo rằng bạn thiết lập dự án của mình để có thể sử dụng các kiểu Stylus (hoặc bạn có thể thiết lập để sử dụng Sass và đổi lang thành scss cho các khối kiểu và nó sẽ hoạt động theo cách đó). Bạn cũng có thể xem những gì chúng tôi đang xây dựng ngày hôm nay trên trang demo.

Lưu ý: Tôi sẽ cho rằng người đọc có kiến thức vững chắc về JavaScript và nắm bắt tốt các tính năng và API của Vue, đặc biệt là API thành phần của Vue 3, nhưng chưa chắc họ biết cách tốt nhất để sử dụng chúng. Bài viết này sẽ hướng dẫn cách tạo trình tải lên kéo và thả trong bối cảnh ứng dụng Vue trong khi thảo luận về các mẫu và phương pháp hay và sẽ không đi sâu vào cách sử dụng Vue.

Thiết lập​

Có rất nhiều cách để thiết lập dự án Vue: Vue CLI, Vite, NuxtQuasar đều có các công cụ tạo khung dự án riêng và tôi chắc rằng còn nhiều hơn nữa. Tôi không quen thuộc lắm với hầu hết các công cụ này và tôi sẽ không chỉ định bất kỳ công cụ nào phù hợp cho dự án này, vì vậy tôi khuyên bạn nên đọc tài liệu hướng dẫn cho bất kỳ công cụ nào bạn chọn để tìm ra cách thiết lập theo cách chúng ta cần cho dự án nhỏ này.

Chúng ta cần thiết lập Vue 3 với cú pháp thiết lập tập lệnh và nếu bạn lấy các kiểu của tôi từ kho Github, bạn sẽ cần đảm bảo rằng mình đã thiết lập để biên dịch các kiểu Vue từ Stylus (hoặc bạn có thể thiết lập để sử dụng Sass và đổi lang thành "scss" cho các khối kiểu và nó sẽ hoạt động theo cách đó).

Drop Zone​

Bây giờ chúng ta đã có dự án thiết lập, hãy cùng tìm hiểu mã. Chúng ta sẽ bắt đầu với một thành phần xử lý chức năng kéo và thả. Đây sẽ là một phần tử div bao bọc đơn giản với một loạt trình lắng nghe sự kiện và trình phát phần lớn. Loại phần tử này là ứng cử viên tuyệt vời cho một thành phần có thể tái sử dụng (mặc dù nó chỉ được sử dụng một lần trong dự án cụ thể này): nó có một công việc rất cụ thể để thực hiện và công việc đó đủ chung chung để có thể sử dụng theo nhiều cách/nơi khác nhau mà không cần nhiều tùy chọn tùy chỉnh hoặc phức tạp.

Đây là một trong những điều mà các nhà phát triển giỏi luôn để mắt đến. Nhồi nhét nhiều chức năng vào một thành phần duy nhất sẽ là một ý tưởng tồi cho dự án này hoặc bất kỳ dự án nào khác vì khi đó 1) nó không thể được tái sử dụng nếu sau này bạn gặp phải tình huống tương tự và 2) sẽ khó sắp xếp mã và tìm ra cách từng phần liên quan đến nhau hơn. Vì vậy, chúng ta sẽ làm những gì có thể để tuân theo nguyên tắc này và bắt đầu từ đây với thành phần DropZone. Chúng ta sẽ bắt đầu với phiên bản đơn giản của thành phần này và sau đó chỉnh sửa một chút để giúp bạn hiểu rõ hơn về những gì đang diễn ra, vì vậy hãy tạo tệp DropZone.vue trong thư mục src/components:
Mã:
   import { onMounted, onUnmounted } từ 'vue'const emit = defineEmits(['files-dropped'])function onDrop(e) { emit('files-dropped', [...e.dataTransfer.files])}function preventDefaults(e) { e.preventDefault()}const events = ['dragenter', 'dragover', 'dragleave', 'drop']onMounted(() => { events.forEach((eventName) => { document.body.addEventListener(eventName, preventDefaults) })})onUnmounted(() => { events.forEach((eventName) => { document.body.removeEventListener(eventName, preventDefaults) })})
Đầu tiên, khi xem mẫu, bạn sẽ thấy một div với trình xử lý sự kiện drop (với trình sửa đổi prevent để ngăn chặn các hành động mặc định) gọi một hàm mà chúng ta sẽ tìm hiểu sau. Bên trong div đó là một slot, vì vậy chúng ta có thể sử dụng lại thành phần này với nội dung tùy chỉnh bên trong nó. Sau đó, chúng ta sẽ đến mã JavaScript, nằm bên trong thẻ script với thuộc tính setup.

Bên trong tập lệnh, chúng ta định nghĩa một sự kiện mà chúng ta sẽ phát ra có tên là 'files-dropped' mà các thành phần khác có thể sử dụng để thực hiện một số thao tác với các tệp được thả ở đây. Sau đó, chúng ta định nghĩa hàm onDrop để xử lý sự kiện thả. Hiện tại, tất cả những gì nó làm là phát ra sự kiện mà chúng ta vừa định nghĩa và thêm một mảng các tệp vừa được thả dưới dạng tải trọng. Lưu ý, chúng ta đang sử dụng một mẹo với toán tử spread để chuyển đổi danh sách các tệp từ FileListe.dataTransfer.files cung cấp cho chúng ta thành một mảng File để tất cả các phương thức mảng có thể được gọi trên đó bởi phần hệ thống tiếp nhận các tệp.

Cuối cùng, chúng ta đến nơi chúng ta xử lý các sự kiện kéo/thả khác xảy ra trên phần thân, ngăn chặn hành vi mặc định trong quá trình kéo và thả (cụ thể là nó sẽ mở một trong các tệp trong trình duyệt. Chúng ta tạo một hàm chỉ đơn giản gọi preventDefault trên đối tượng sự kiện. Sau đó, trong móc vòng đời onMounted, chúng ta lặp lại danh sách các sự kiện và ngăn chặn hành vi mặc định cho sự kiện đó ngay cả trên phần thân tài liệu. Trong móc onUnmounted, chúng ta xóa các trình lắng nghe đó.

Trạng thái hoạt động​

Vậy, chúng ta có thể thêm chức năng bổ sung nào? Một điều tôi đã quyết định để thêm một số trạng thái cho biết vùng thả có "hoạt động" hay không, nghĩa là tệp hiện đang di chuột qua vùng thả. Điều đó đủ đơn giản; tạo một ref có tên là active, đặt thành true đối với các sự kiện khi các tệp được kéo qua vùng thả và thành false khi chúng rời khỏi vùng hoặc được thả.

Chúng ta cũng muốn phơi bày trạng thái này cho các thành phần bằng cách sử dụng DropZone, vì vậy chúng ta sẽ biến slot của mình thành một slot có phạm vi và phơi bày trạng thái đó tại đó. Thay vì slot có phạm vi (hoặc ngoài slot để tăng thêm tính linh hoạt), chúng ta có thể phát ra một sự kiện để thông báo cho bên ngoài về giá trị của active khi nó thay đổi. Ưu điểm của việc này là toàn bộ thành phần đang sử dụng DropZone có thể truy cập vào trạng thái, thay vì bị giới hạn ở các thành phần/phần tử trong slot trong mẫu. Tuy nhiên, chúng ta sẽ tiếp tục sử dụng khe có phạm vi cho bài viết này.

Cuối cùng, để chắc chắn, chúng ta sẽ thêm thuộc tính data-active phản ánh giá trị của active để chúng ta có thể khóa nó để tạo kiểu. Bạn cũng có thể sử dụng một lớp nếu muốn, nhưng tôi có xu hướng thích các thuộc tính dữ liệu cho các trình sửa đổi trạng thái.

Chúng ta hãy viết nó ra:
Mã:
     // đảm bảo nhập `ref` từ Vueimport { ref, onMounted, onUnmounted } từ 'vue'const emit = defineEmits(['files-dropped'])// Tạo trạng thái `active` và quản lý nó bằng các hàmlet active = ref(false)function setActive() { active.value = true}function setInactive() { active.value = false}function onDrop(e) { setInactive() // thêm dòng này nữa emit('files-dropped', [...e.dataTransfer.files])}// ... không có gì thay đổi bên dưới
Tôi đã đưa ra một số bình luận trong mã để ghi chú những thay đổi nằm ở đâu, vì vậy tôi sẽ không đi sâu vào vấn đề này, nhưng tôi có một số ghi chú. Chúng tôi đang sử dụng lại các trình sửa đổi prevent trên tất cả các trình lắng nghe sự kiện để đảm bảo rằng hành vi mặc định không được kích hoạt. Ngoài ra, bạn sẽ nhận thấy rằng các hàm setActivesetInactive có vẻ hơi quá mức vì bạn có thể chỉ cần đặt active trực tiếp và bạn chắc chắn có thể đưa ra lập luận đó, nhưng hãy đợi một chút; sẽ có một thay đổi khác thực sự biện minh cho việc tạo các hàm.

Bạn thấy đấy, có một vấn đề với những gì chúng ta đã làm. Như bạn có thể thấy trong video bên dưới, sử dụng mã này cho vùng thả có nghĩa là nó có thể nhấp nháy giữa trạng thái hoạt động và không hoạt động trong khi bạn kéo một thứ gì đó xung quanh bên trong vùng thả.

[*] Tương tác kéo nhấp nháy
Tại sao lại như vậy? Khi bạn kéo một thứ gì đó qua một phần tử con, nó sẽ "vào" phần tử đó và "rời" khỏi vùng thả, khiến nó trở nên không hoạt động. Sự kiện dragenter sẽ nổi lên vùng thả, nhưng nó xảy ra trước sự kiện dragleave, vì vậy điều đó không có ích. Sau đó, sự kiện dragover sẽ kích hoạt lại trên vùng thả, sự kiện này sẽ lật ngược vùng thả trở lại trạng thái hoạt động nhưng không phải trước khi nhấp nháy sang trạng thái không hoạt động.

Để khắc phục sự cố này, chúng ta sẽ thêm một khoảng thời gian chờ ngắn vào hàm setInactive để ngăn hàm này không chuyển sang trạng thái không hoạt động ngay lập tức. Sau đó, setActive sẽ xóa khoảng thời gian chờ đó để nếu hàm này được gọi trước khi chúng ta thực sự đặt thành không hoạt động, thì nó sẽ không thực sự trở thành không hoạt động. Hãy thực hiện những thay đổi đó:
Mã:
// Không có gì thay đổi ở trênlet active = ref(false)let inActiveTimeout = null // thêm một biến để giữ khóa thời gian chờfunction setActive() { active.value = true clearTimeout(inActiveTimeout) // xóa thời gian chờ}function setInactive() { // bọc nó trong `setTimeout` inActiveTimeout = setTimeout(() => { active.value = false }, 50)}// Không có gì bên dưới thay đổi này
Bạn sẽ thấy thời gian chờ là 50 mili giây. Tại sao lại là con số này? Bởi vì tôi đã thử nghiệm một số thời gian chờ khác nhau và cảm thấy đây là con số tốt nhất.

Tôi biết điều đó là chủ quan nhưng hãy lắng nghe tôi. Tôi đã thử nghiệm thời gian chờ nhỏ hơn nhiều và 15ms là mức thấp nhất mà tôi từng thấy, nhưng ai biết điều đó sẽ hoạt động như thế nào trên phần cứng khác? Theo tôi, biên độ sai số của nó quá nhỏ. Bạn cũng có thể không muốn vượt quá 100ms vì điều đó có thể gây ra độ trễ nhận thức khi người dùng cố tình làm điều gì đó nên khiến nó không hoạt động. Cuối cùng, tôi đã quyết định chọn một khoảng thời gian ở giữa đủ dài để đảm bảo rằng sẽ không có hiện tượng nhấp nháy trên bất kỳ phần cứng nào và không có độ trễ nhận thức nào.

Đó là tất cả những gì chúng ta cần cho thành phần DropZone, vì vậy hãy chuyển sang phần tiếp theo của câu đố: trình quản lý danh sách tệp.

Trình quản lý danh sách tệp​

Tôi đoán điều đầu tiên cần làm là giải thích ý tôi khi nói đến trình quản lý danh sách tệp. Đây sẽ là một hàm thành phần trả về một số phương thức để quản lý trạng thái của các tệp mà người dùng đang cố gắng tải lên. Điều này cũng có thể được triển khai như một kho lưu trữ Vuex/Pinia/thay thế, nhưng để giữ mọi thứ đơn giản và tránh phải cài đặt phụ thuộc nếu chúng ta không cần, thì việc giữ nó như một hàm biên soạn là rất hợp lý, đặc biệt là vì dữ liệu có thể không cần thiết rộng rãi trên toàn bộ ứng dụng, đó là nơi các kho lưu trữ hữu ích nhất.

Bạn cũng có thể chỉ cần xây dựng chức năng trực tiếp vào thành phần sẽ sử dụng thành phần DropZone của chúng ta, nhưng chức năng này có vẻ như là thứ có thể dễ dàng được tái sử dụng; việc kéo nó ra khỏi thành phần giúp thành phần dễ hiểu hơn về mục đích của những gì đang diễn ra (giả sử tên hàm và tên biến tốt) mà không cần phải lội qua toàn bộ phần triển khai.

Bây giờ chúng ta đã làm rõ rằng đây sẽ là một hàm thành phần và lý do tại sao, đây là những gì trình quản lý danh sách tệp sẽ thực hiện:
  1. Giữ danh sách các tệp đã được người dùng chọn;
  2. Ngăn chặn các tệp trùng lặp;
  3. Cho phép chúng tôi xóa các tệp khỏi danh sách;
  4. Tăng cường các tệp bằng siêu dữ liệu hữu ích: ID, URL có thể được sử dụng để hiển thị bản xem trước của tệp và trạng thái tải lên của tệp.
Vì vậy, hãy xây dựng nó trong src/compositions/file-list.js:
Mã:
import { ref } từ 'vue'export default function () { const files = ref([]) function addFiles(newFiles) { let newUploadableFiles = [...newFiles] .map((file) => new UploadableFile(file)) .filter((file) => !fileExists(file.id)) files.value = files.value.concat(newUploadableFiles) } function fileExists(otherId) { return files.value.some(({ id }) => id === otherId) } function removeFile(file) { const index = files.value.indexOf(file) if (index > -1) files.value.splice(index, 1) } return { files, addFiles, removeFile }}class UploadableFile { constructor(file) { this.file = file this.id = `${file.name}-${file.size}-${file.lastModified}-${file.type}` this.url = URL.createObjectURL(file) this.status = null }}
Theo mặc định, chúng ta đang xuất một hàm trả về danh sách tệp (dưới dạng ref) và một vài phương thức được sử dụng để thêm và xóa tệp khỏi danh sách. Sẽ rất tuyệt nếu danh sách tệp được trả về dưới dạng chỉ đọc để buộc bạn phải sử dụng các phương thức để thao tác danh sách, bạn có thể thực hiện khá dễ dàng bằng cách sử dụng hàm readonly được nhập từ Vue, nhưng điều đó sẽ gây ra sự cố với trình tải lên mà chúng ta sẽ xây dựng sau.

Lưu ý rằng files được giới hạn trong hàm thành phần và được đặt bên trong hàm đó, vì vậy mỗi lần bạn gọi hàm, bạn sẽ nhận được một danh sách tệp mới. Nếu bạn muốn chia sẻ trạng thái trên nhiều thành phần/lệnh gọi, thì bạn sẽ cần phải kéo khai báo đó ra khỏi hàm để nó được định phạm vi và thiết lập một lần trong mô-đun, nhưng trong trường hợp của chúng ta, chúng ta chỉ sử dụng nó một lần, vì vậy nó không thực sự quan trọng và tôi đã làm việc theo suy nghĩ rằng mỗi phiên bản của danh sách tệp sẽ được sử dụng bởi một trình tải lên riêng biệt và bất kỳ trạng thái nào cũng có thể được truyền xuống các thành phần con thay vì được chia sẻ thông qua hàm thành phần.

Phần phức tạp nhất của trình quản lý danh sách tệp này là thêm các tệp mới vào danh sách. Trước tiên, chúng ta đảm bảo rằng nếu một đối tượng FileList được truyền thay vì một mảng các đối tượng File, thì chúng ta sẽ chuyển đổi nó thành một mảng (như chúng ta đã làm trong DropZone khi chúng ta phát ra các tệp. Điều này có nghĩa là chúng ta có thể bỏ qua quá trình chuyển đổi đó, nhưng tốt hơn là an toàn hơn là xin lỗi). Sau đó, chúng ta chuyển đổi tệp thành UploadableFile, đây là một lớp mà chúng ta đang định nghĩa để gói tệp và cung cấp cho chúng ta một số thuộc tính bổ sung. Chúng tôi đang tạo một id dựa trên một số khía cạnh của tệp để chúng tôi có thể phát hiện các bản sao, một URL blob:// của hình ảnh để chúng tôi có thể hiển thị hình thu nhỏ xem trước và trạng thái để theo dõi lượt tải lên.

Bây giờ chúng tôi đã có ID trên các tệp, chúng tôi lọc ra bất kỳ tệp nào đã tồn tại trong danh sách tệp trước khi nối chúng vào cuối danh sách tệp.

Những cải tiến có thể thực hiện​

Mặc dù trình quản lý danh sách tệp này hoạt động tốt với chức năng của nó, nhưng vẫn có một số nâng cấp có thể thực hiện được. Một mặt, thay vì gói tệp trong một lớp mới rồi phải gọi .file trên đó để truy cập đối tượng tệp gốc, chúng ta có thể gói tệp trong một proxy chỉ định các thuộc tính mới của mình, nhưng sau đó sẽ chuyển tiếp mọi yêu cầu thuộc tính khác đến đối tượng gốc, do đó liền mạch hơn.

Một giải pháp thay thế cho việc gói từng tệp trong UploadableFile, chúng ta có thể cung cấp các hàm tiện ích có thể trả về ID hoặc URL đã cho một tệp, nhưng cách đó kém tiện lợi hơn một chút và có nghĩa là bạn có khả năng tính toán các thuộc tính này nhiều lần (cho mỗi lần kết xuất, v.v.), nhưng điều đó thực sự không quan trọng trừ khi bạn đang xử lý việc mọi người thả hàng nghìn hình ảnh cùng một lúc, trong trường hợp đó, bạn có thể thử ghi nhớ nó.

Về trạng thái, trạng thái đó không được lấy trực tiếp từ File, do đó, một hàm tiện ích đơn giản như những hàm khác sẽ không khả thi, nhưng bạn có thể lưu trữ trạng thái của từng tệp bằng trình tải lên (chúng ta sẽ xây dựng chức năng đó sau) thay vì lưu trực tiếp với các tệp. Đây có thể là cách xử lý tốt hơn trong một ứng dụng lớn để chúng ta không phải điền vào lớp UploadableFile một loạt các thuộc tính chỉ hỗ trợ một khu vực duy nhất của ứng dụng và vô dụng ở những nơi khác.

Lưu ý: Đối với mục đích của chúng tôi, việc có các thuộc tính có sẵn trực tiếp trên đối tượng tệp của chúng tôi là thuận tiện nhất, nhưng chắc chắn có thể lập luận rằng nó không phải là cách phù hợp nhất.

Một cải tiến khả thi khác là cho phép bạn chỉ định bộ lọc để chỉ cho phép thêm một số loại tệp nhất định vào danh sách. Điều này cũng yêu cầu addFiles trả về lỗi khi một số tệp không khớp với bộ lọc để cho người dùng biết họ đã mắc lỗi. Đây chắc chắn là điều cần thực hiện trong các ứng dụng sẵn sàng sản xuất.

Tốt hơn khi kết hợp​

Chúng ta còn lâu mới có một sản phẩm hoàn thiện, nhưng hãy ghép các phần chúng ta có lại với nhau để xác minh mọi thứ đang hoạt động cho đến nay. Chúng ta sẽ chỉnh sửa tệp /src/App.vue để đưa các phần này vào, nhưng bạn có thể thêm chúng vào bất kỳ thành phần trang/phần nào bạn muốn. Tuy nhiên, nếu bạn đặt nó bên trong một thành phần thay thế, hãy bỏ qua bất kỳ thứ gì (như ID của "ứng dụng") mà chỉ có thể nhìn thấy trên thành phần ứng dụng chính.
Mã:
    Drop Them   Kéo tệp của bạn vào đây   import useFileList from './compositions/file-list'import DropZone from './components/DropZone.vue'const { files, addFiles, removeFile } = useFileList()
Nếu bạn bắt đầu với phần script, bạn sẽ thấy chúng ta không làm gì nhiều. Chúng ta đang nhập hai tệp mà chúng ta vừa hoàn thành việc viết và chúng ta đang khởi tạo danh sách tệp. Lưu ý, chúng ta chưa sử dụng files hoặc removeFile, nhưng chúng ta sẽ sử dụng sau, vì vậy tôi chỉ giữ chúng ở đó hiện tại. Xin lỗi nếu ESLint phàn nàn về các biến không sử dụng. Chúng ta sẽ muốn files ít nhất để chúng ta có thể xem nó có hoạt động sau này không.

Chuyển sang mẫu, bạn có thể thấy chúng ta đang sử dụng thành phần DropZone ngay lập tức. Chúng ta đang cung cấp cho nó một lớp để chúng ta có thể định kiểu cho nó, truyền hàm addFiles cho trình xử lý sự kiện "files-dropped" và lấy biến khe có phạm vi để nội dung của chúng ta có thể động dựa trên việc vùng thả có hoạt động hay không. Sau đó, bên trong khe của vùng thả, chúng ta tạo một div hiển thị thông báo để kéo tệp qua nếu nó không hoạt động và thông báo để thả chúng khi nó hoạt động.

Bây giờ, có thể bạn sẽ muốn một số kiểu để ít nhất làm cho vùng thả lớn hơn và dễ tìm hơn. Tôi sẽ không dán bất kỳ kiểu nào ở đây, nhưng bạn có thể tìm thấy các kiểu tôi đã sử dụng cho App.vue trong kho lưu trữ.

Bây giờ, trước khi chúng ta có thể kiểm tra trạng thái hiện tại của ứng dụng, chúng ta sẽ cần phiên bản beta của Vue DevTools được cài đặt trong trình duyệt của mình (phiên bản ổn định vẫn chưa hỗ trợ Vue 3). Bạn có thể tải Vue DevTools từ cửa hàng web Chrome cho hầu hết các trình duyệt dựa trên Chromium hoặc tải xuống Vue DevTools tại đây cho Firefox.

Sau khi cài đặt, hãy chạy ứng dụng của bạn bằng npm run serve (Vue CLI), npm run dev (Vite) hoặc bất kỳ tập lệnh nào bạn sử dụng trong ứng dụng của mình, sau đó mở ứng dụng đó trong trình duyệt của bạn thông qua URL được cung cấp trong dòng lệnh. Mở Vue DevTools, sau đó kéo và thả một số hình ảnh vào vùng thả. Nếu thành công, bạn sẽ thấy một mảng gồm nhiều tệp bạn đã thêm khi xem thành phần chúng ta vừa viết (xem ảnh chụp màn hình bên dưới).



Tuyệt! Bây giờ chúng ta hãy làm cho mục này dễ truy cập hơn một chút đối với những người dùng không thể (hoặc không muốn) kéo và thả, bằng cách thêm một mục nhập tệp ẩn (sẽ hiển thị khi được tập trung qua bàn phím đối với những người cần, giả sử bạn đang sử dụng kiểu của tôi) và bao quanh mọi thứ bằng một nhãn lớn để cho phép chúng ta sử dụng nó mặc dù nó không hiển thị. Cuối cùng, chúng ta sẽ cần thêm một trình lắng nghe sự kiện vào tệp đầu vào để khi người dùng chọn một tệp, chúng ta có thể thêm tệp đó vào danh sách tệp của mình.

Chúng ta hãy bắt đầu với những thay đổi đối với phần script. Chúng ta sẽ chỉ thêm một hàm vào cuối phần đó:
Mã:
function onInputChange(e) { addFiles(e.target.files) e.target.value = null}
Hàm này xử lý sự kiện "thay đổi" được kích hoạt từ đầu vào và thêm các tệp từ đầu vào vào danh sách tệp. Lưu ý dòng cuối cùng trong hàm đặt lại giá trị của đầu vào. Nếu người dùng thêm tệp thông qua đầu vào, quyết định xóa tệp khỏi danh sách tệp của chúng ta, sau đó đổi ý và quyết định sử dụng đầu vào để thêm tệp đó một lần nữa, thì đầu vào tệp sẽ không kích hoạt sự kiện "thay đổi" vì đầu vào tệp không thay đổi. Bằng cách đặt lại giá trị như thế này, chúng ta đảm bảo sự kiện sẽ luôn được kích hoạt.

Bây giờ, hãy thực hiện các thay đổi đối với mẫu. Thay đổi toàn bộ mã bên trong khe DropZone thành mã sau:
Mã:
  Drop Them Here để thêm chúng   Kéo tệp của bạn vào đây  hoặc [B][I]nhấp vào đây[/I][/b] để chọn tệp
Chúng tôi gói toàn bộ thứ đó trong một nhãn được liên kết với đầu vào tệp, sau đó chúng tôi thêm các thông báo động của mình trở lại, mặc dù tôi đã thêm một chút thông báo để thông báo cho người dùng rằng họ có thể nhấp để chọn tệp. Tôi cũng đã thêm một chút cho thông báo "thả chúng" để chúng có cùng số dòng văn bản để vùng thả sẽ không thay đổi kích thước khi hoạt động. Cuối cùng, chúng tôi thêm đầu vào tệp, đặt thuộc tính multiple để cho phép người dùng chọn nhiều tệp cùng một lúc, sau đó kết nối trình lắng nghe sự kiện "thay đổi" với hàm mà chúng tôi vừa viết.

Chạy lại ứng dụng, nếu bạn đã dừng ứng dụng, chúng ta sẽ thấy cùng một kết quả trong Vue DevTools cho dù chúng ta kéo và thả tệp hay nhấp vào hộp để sử dụng trình chọn tệp.

Xem trước hình ảnh đã chọn​

Tuyệt, nhưng người dùng sẽ không sử dụng Vue DevTools để xem các tệp họ thả có thực sự được thêm vào hay không, vì vậy hãy bắt đầu hiển thị những tệp đó cho người dùng. Chúng ta sẽ bắt đầu bằng cách chỉnh sửa App.vue (hoặc bất kỳ tệp thành phần nào mà bạn đã thêm DropZone vào) và hiển thị danh sách văn bản đơn giản với tên tệp.

Hãy thêm đoạn mã sau vào mẫu ngay sau label mà chúng ta vừa thêm ở bước trước:
Mã:
[LIST] 
[*] {{ file.file.name }}[/LIST]
Bây giờ, khi ứng dụng đang chạy, nếu bạn thêm một số tệp vào danh sách, bạn sẽ thấy danh sách tên tệp được đánh dấu đầu dòng. Nếu bạn sao chép kiểu của tôi, trông có vẻ hơi lạ, nhưng không sao cả vì chúng tôi sẽ sớm thay đổi. Lưu ý rằng nhờ thêm ID của tệp vào trình quản lý danh sách tệp, giờ chúng ta có một khóa trong vòng lặp. Điều duy nhất làm tôi khó chịu là vì chúng ta đã gói các tệp, chúng ta cần phải viết file.file để truy cập đối tượng tệp gốc để lấy tên của nó. Tuy nhiên, cuối cùng, đây là một sự hy sinh nhỏ.

Bây giờ, chúng ta hãy bắt đầu hiển thị hình ảnh thay vì chỉ liệt kê tên của chúng, nhưng đã đến lúc di chuyển chức năng này ra khỏi thành phần chính này. Chúng tôi chắc chắn có thể tiếp tục đặt chức năng xem trước tệp ở đây, nhưng có hai lý do chính đáng để loại bỏ nó:
  1. Chức năng này có khả năng tái sử dụng trong các trường hợp khác.
  2. Khi chức năng này mở rộng, việc tách nó ra sẽ ngăn thành phần chính trở nên quá phình to.
Vì vậy, hãy tạo /src/FilePreview.vue để đưa chức năng này vào và chúng ta sẽ bắt đầu bằng cách chỉ hiển thị hình ảnh trong một trình bao bọc.
Mã:
[IMG]file.url[/IMG]
 defineProps({ file: { type: Object, required: true }, tag: { type: String, default: 'li' },})
Một lần nữa, các kiểu không được bao gồm ở đây, nhưng bạn có thể tìm thấy chúng trên GitHub. Tuy nhiên, điều đầu tiên cần lưu ý về mã chúng ta có là chúng ta đang gói mã này trong thẻ component và đặt loại thẻ đó là gì bằng tag prop. Đây có thể là một cách tốt để làm cho một thành phần chung chung hơn và có thể tái sử dụng được. Hiện tại, chúng ta đang sử dụng mã này bên trong một danh sách không có thứ tự, vì vậy li là lựa chọn hiển nhiên, nhưng nếu chúng ta muốn sử dụng thành phần này ở một nơi khác tại một thời điểm nào đó, thì thành phần này có thể không nằm trong danh sách, vì vậy chúng ta sẽ muốn có một thẻ khác.

Đối với hình ảnh, chúng ta sử dụng URL do trình quản lý danh sách tệp tạo ra và sử dụng tên tệp làm văn bản thay thế và làm thuộc tính title để có chức năng miễn phí cho phép người dùng di chuột qua hình ảnh và xem tên tệp dưới dạng chú giải công cụ. Tất nhiên, bạn luôn có thể tạo bản xem trước tệp của riêng mình, trong đó tên tệp được viết ra để người dùng luôn nhìn thấy. Chắc chắn có rất nhiều sự tự do trong cách xử lý việc này.

Chuyển sang JavaScript, chúng ta thấy các thuộc tính được định nghĩa để có thể truyền vào tệp mà chúng ta đang xem trước và tên thẻ để tùy chỉnh trình bao bọc nhằm sử dụng được trong nhiều tình huống hơn.

Tất nhiên, nếu bạn thử chạy lệnh này, có vẻ như nó không có tác dụng gì vì hiện tại chúng ta không sử dụng các thành phần FilePreview. Chúng ta hãy khắc phục điều đó ngay bây giờ. Trong mẫu, hãy thay thế danh sách hiện tại bằng danh sách sau:
Mã:
[LIST] [/LIST]
Ngoài ra, chúng ta cần nhập thành phần mới của mình vào phần script:
Mã:
import FilePreview from './components/FilePreview.vue'
Bây giờ, nếu bạn chạy lệnh này, bạn sẽ thấy một số hình thu nhỏ đẹp mắt của mỗi hình ảnh bạn thả hoặc chọn.

Xóa tệp khỏi danh sách​

Chúng ta hãy tăng cường khả năng xóa tệp khỏi danh sách. Chúng ta sẽ thêm một nút có dấu “X” ở góc của hình ảnh mà mọi người có thể nhấp/chạm vào để xóa hình ảnh. Để thực hiện việc này, chúng ta cần thêm 2 dòng mã vào FilePreview.vue. Trong mẫu, ngay phía trên thẻ img, hãy thêm nội dung sau:
Mã:
×
Sau đó, thêm dòng này vào đâu đó trong phần script:
Mã:
defineEmits(['remove'])
Bây giờ, nhấp vào nút đó sẽ kích hoạt sự kiện remove, truyền tệp dưới dạng tải trọng. Bây giờ, chúng ta cần quay lại thành phần ứng dụng chính để xử lý sự kiện đó. Tất cả những gì chúng ta cần làm là thêm trình lắng nghe sự kiện vào thẻ FilePreview:
Mã:
Nhờ removeFile đã được định nghĩa bởi trình quản lý danh sách tệp và sử dụng cùng các đối số mà chúng ta truyền từ sự kiện, chúng ta hoàn thành trong vài giây. Bây giờ, nếu bạn chạy ứng dụng và chọn một số hình ảnh, bạn có thể nhấp vào "X" nhỏ và hình ảnh tương ứng sẽ biến mất khỏi danh sách.

Những cải tiến có thể thực hiện​

Như thường lệ, có những cải tiến có thể thực hiện đối với điều này nếu bạn muốn và ứng dụng của bạn có thể sử dụng lại thành phần này ở nơi khác nếu nó chung chung hơn hoặc có thể tùy chỉnh.

Trước hết, bạn có thể quản lý các kiểu tốt hơn. Tôi biết rằng tôi đã không đăng các kiểu ở đây, nhưng nếu bạn sao chép chúng từ GitHub và bạn là người rất quan tâm đến thành phần nào kiểm soát kiểu nào, thì bạn có thể nghĩ rằng sẽ khôn ngoan hơn khi di chuyển một số tệp cụ thể ra khỏi thành phần này. Cũng như hầu hết các cải tiến khả thi này, điều này chủ yếu liên quan đến việc làm cho thành phần hữu ích hơn trong nhiều tình huống hơn. Một số kiểu rất cụ thể đối với cách tôi muốn hiển thị bản xem trước cho ứng dụng nhỏ này, nhưng để có thể tái sử dụng nhiều hơn, chúng ta cần tùy chỉnh các kiểu thông qua props hoặc kéo chúng ra và để một thành phần bên ngoài xác định các kiểu.

Một thay đổi khả thi khác là thêm props cho phép bạn ẩn một số thành phần nhất định như nút kích hoạt sự kiện "xóa". Có nhiều thành phần khác sẽ xuất hiện sau trong bài viết mà có thể ẩn thông qua props.

Và cuối cùng, có thể khôn ngoan khi tách prop file thành nhiều props như url, name và — như chúng ta sẽ thấy sau — status. Điều này sẽ cho phép sử dụng thành phần này trong các tình huống mà bạn chỉ có URL và tên hình ảnh thay vì một thể hiện UploadableFile, do đó, nó hữu ích hơn trong nhiều tình huống.

Tải tệp lên​

Được rồi, chúng ta đã chọn kéo và thả và xem trước các tệp, vì vậy bây giờ chúng ta cần tải các tệp đó lên và thông báo cho người dùng về trạng thái của các lần tải lên đó. Chúng ta sẽ bắt đầu bằng cách tạo một tệp mới: /compositions/file-uploader.js. Trong tệp này, chúng ta sẽ xuất một số hàm cho phép thành phần của chúng ta tải các tệp lên.
Mã:
export async function uploadFile(file, url) { // thiết lập dữ liệu yêu cầu let formData = new FormData() formData.append('file', file.file) // theo dõi trạng thái và tải tệp lên file.status = 'loading' let response = await fetch(url, { method: 'POST', body: formData }) // thay đổi trạng thái để chỉ ra thành công của yêu cầu tải lên file.status = response.ok return response}export function uploadFiles(files, url) { return Promise.all(files.map((file) => uploadFile(file, url)))}export default function createUploader(url) { return { uploadFile: function (file) { return uploadFile(file, url) }, uploadFiles: function (files) { return uploadFiles(files, url) }, }}
Trước khi xem xét các hàm cụ thể, hãy lưu ý rằng mọi hàm trong tệp này đều được xuất riêng để có thể sử dụng trên own, nhưng bạn sẽ thấy rằng chúng ta sẽ chỉ sử dụng một trong số chúng trong ứng dụng của mình. Điều này mang lại một số tính linh hoạt trong cách sử dụng mô-đun này mà không thực sự làm cho mã phức tạp hơn vì tất cả những gì chúng ta làm là thêm một câu lệnh export để kích hoạt nó.

Bây giờ, bắt đầu từ đầu, chúng ta có một hàm bất đồng bộ để tải lên một tệp duy nhất. Hàm này được xây dựng theo cách rất giống với cách thực hiện trong bài viết trước, nhưng chúng ta đang sử dụng hàm async thay vào đó (cho từ khóa await tuyệt vời đó) và chúng ta đang cập nhật thuộc tính status trên file được cung cấp để theo dõi tiến trình tải lên. status này có thể có 4 giá trị có thể:
  • null: giá trị ban đầu; chỉ ra rằng nó chưa bắt đầu tải lên;
  • "loading": chỉ ra rằng quá trình tải lên đang diễn ra;
  • true: chỉ ra rằng quá trình tải lên đã thành công;
  • false: chỉ ra rằng quá trình tải lên không thành công.
Vì vậy, khi chúng ta bắt đầu tải lên, chúng ta đánh dấu trạng thái là "loading". Sau khi hoàn tất, chúng ta đánh dấu là true hoặc false tùy thuộc vào thuộc tính ok của kết quả. Chúng ta sẽ sớm sử dụng các giá trị này để hiển thị các thông báo khác nhau trong thành phần FilePreview. Cuối cùng, chúng ta trả về phản hồi trong trường hợp người gọi có thể sử dụng thông tin đó.

Lưu ý: Tùy thuộc vào dịch vụ bạn tải tệp lên, bạn có thể cần một số tiêu đề bổ sung để ủy quyền hoặc thứ gì đó, nhưng bạn có thể lấy những tiêu đề đó từ tài liệu cho các dịch vụ đó vì tôi không thể viết ví dụ cho mọi dịch vụ ngoài kia.

Hàm tiếp theo, uploadFiles, ở đó để cho phép bạn dễ dàng tải lên một mảng tệp. Hàm cuối cùng, createUploader, là một hàm cấp cho bạn khả năng sử dụng các hàm khác mà không cần phải chỉ định URL mà bạn đang tải lên mỗi khi bạn gọi hàm đó. Hàm này "lưu trữ đệm" URL thông qua một closure và trả về các phiên bản của mỗi hàm trước đó không yêu cầu tham số URL được truyền vào.

Sử dụng Uploader​

Bây giờ chúng ta đã định nghĩa các hàm này, chúng ta cần sử dụng chúng, vì vậy hãy quay lại thành phần ứng dụng chính của chúng ta. Ở đâu đó trong phần script, chúng ta sẽ cần thêm hai dòng sau:
Mã:
import createUploader from './compositions/file-uploader'const { uploadFiles } = createUploader('YOUR URL HERE')
Tất nhiên, bạn sẽ cần thay đổi URL để phù hợp với bất kỳ máy chủ tải lên nào của bạn sử dụng. Bây giờ chúng ta chỉ cần gọi uploadFiles từ một nơi nào đó, vì vậy hãy thêm một nút gọi nó trong trình xử lý nhấp chuột của nó. Thêm nội dung sau vào cuối mẫu:
Mã:
Upload
Vậy là xong. Bây giờ nếu bạn chạy ứng dụng, thêm một số hình ảnh và nhấn nút đó, chúng sẽ hướng đến máy chủ. Nhưng… chúng ta không thể biết nó có hoạt động hay không — ít nhất là không thể nếu không kiểm tra máy chủ hoặc bảng điều khiển mạng trong công cụ phát triển. Hãy sửa lỗi đó.

Hiển thị trạng thái​

Mở FilePreview.vue. Trong mẫu sau thẻ img nhưng vẫn nằm trong component, hãy thêm nội dung sau:
Mã:
Đang tiến hànhĐã tải lênError
Tất cả các kiểu đã được bao gồm để kiểm soát cách chúng trông như thế nào nếu bạn đã sao chép các kiểu từ GitHub trước đó. Tất cả chúng đều nằm ở góc dưới bên phải của hình ảnh hiển thị trạng thái hiện tại. Chỉ một trong số chúng được hiển thị tại một thời điểm dựa trên file.status.

Tôi đã sử dụng v-show ở đây, nhưng cũng rất hợp lý khi sử dụng v-if, vì vậy bạn có thể sử dụng cả hai. Bằng cách sử dụng v-show, nó luôn có các phần tử trong DOM nhưng ẩn chúng đi. Điều này có nghĩa là chúng ta có thể kiểm tra các phần tử và khiến chúng hiển thị ngay cả khi chúng không ở trạng thái chính xác, vì vậy chúng ta có thể kiểm tra xem chúng có đúng không mà không cần cố gắng thực hiện bằng cách đưa ứng dụng vào một trạng thái nhất định. Ngoài ra, bạn có thể vào Vue DevTools, đảm bảo rằng bạn đang ở màn hình "Inspector", nhấp vào nút menu ba chấm ở trên cùng bên phải và chuyển "Editable props" thành true, sau đó chỉnh sửa props hoặc trạng thái trong thành phần để tạo ra các trạng thái cần thiết để kiểm tra từng chỉ báo.

Lưu ý: Chỉ cần lưu ý rằng sau khi bạn chỉnh sửa trạng thái/props file, nó không còn là đối tượng giống với đối tượng đã được truyền vào nữa, do đó, việc nhấp vào nút để xóa hình ảnh sẽ không hiệu quả (không thể xóa tệp không có trong mảng) và việc nhấp vào "Tải lên" sẽ không hiển thị bất kỳ thay đổi trạng thái nào cho hình ảnh đó (vì đối tượng trong mảng đang được tải lên không phải là đối tượng tệp giống với đối tượng được hiển thị trong bản xem trước).

Những cải tiến có thể thực hiện​

Cũng như các phần khác của ứng dụng này, có một số điều chúng ta có thể thực hiện để cải thiện ứng dụng này, nhưng chúng ta sẽ không thực sự thay đổi. Trước hết, các giá trị trạng thái khá mơ hồ. Sẽ là một ý tưởng hay nếu triển khai các giá trị dưới dạng hằng số hoặc enum (TypeScript hỗ trợ enum). Điều này sẽ đảm bảo rằng bạn không viết sai chính tả một giá trị như "đang tải" hoặc cố gắng đặt trạng thái thành "lỗi" thay vì sai và gặp phải lỗi. Trạng thái cũng có thể được triển khai dưới dạng máy trạng thái vì có một tập hợp các quy tắc được xác định rất rõ ràng về cách trạng thái thay đổi.

Ngoài các trạng thái tốt hơn, cần phải có cách xử lý lỗi tốt hơn. Chúng tôi thông báo cho người dùng rằng đã có sự cố khi tải lên, nhưng họ không biết lỗi là gì. Có phải do mạng internet của họ gặp sự cố không? Tệp có quá lớn không? Máy chủ có bị sập không? Ai biết được? Người dùng cần biết sự cố là gì để họ biết mình có thể làm gì về vấn đề đó — nếu có thể.

Chúng tôi cũng có thể thông báo cho người dùng tốt hơn về quá trình tải lên. Bằng cách sử dụng XHR thay vì fetch (mà tôi đã thảo luận trong bài viết về trình tải lên kéo và thả trước đó), chúng ta có thể theo dõi các sự kiện "tiến trình" để biết tỷ lệ phần trăm tải lên đã hoàn tất, điều này rất hữu ích đối với các tệp lớn và kết nối internet chậm vì nó có thể chứng minh với người dùng rằng tiến trình thực sự đang được thực hiện và nó không bị kẹt.

Một thay đổi có thể tăng khả năng tái sử dụng của mã là mở trình tải lên tệp cho các tùy chọn bổ sung (chẳng hạn như tiêu đề yêu cầu) để có thể được truyền vào. Ngoài ra, chúng ta có thể kiểm tra trạng thái của tệp để ngăn chúng ta tải lên tệp đang trong quá trình hoặc đã được tải lên. Để hỗ trợ thêm cho việc này, chúng ta có thể tắt nút "Tải lên" trong khi tải lên và có lẽ cũng nên tắt nút này khi không có tệp nào được chọn.

Và cuối cùng nhưng chắc chắn không kém phần quan trọng, chúng ta nên thêm một số cải tiến về khả năng truy cập. Đặc biệt, khi thêm tệp, xóa tệp và tải tệp lên (với tất cả các thay đổi trạng thái đó), chúng ta nên thông báo bằng giọng nói cho người dùng trình đọc màn hình rằng mọi thứ đã thay đổi bằng cách sử dụng Live Regions. Tôi không phải là chuyên gia về vấn đề này và chúng nằm ngoài phạm vi của bài viết này một chút, vì vậy tôi sẽ không đi sâu vào bất kỳ chi tiết nào, nhưng chắc chắn đây là điều mà mọi người nên tìm hiểu.

Hoàn thành công việc​

Vâng, vậy là xong. Trình tải lên hình ảnh kéo và thả của Vue đã hoàn thành! Như đã đề cập ở phần đầu, bạn có thể xem sản phẩm hoàn thiện tại đây và xem mã cuối cùng trong GitHub Repository.

Tôi hy vọng bạn dành chút thời gian để thử triển khai các cải tiến khả thi mà tôi đã nêu trong các phần trước để giúp bạn hiểu sâu hơn về ứng dụng này và tiếp tục nâng cao kỹ năng của mình bằng cách tự mình suy nghĩ thấu đáo. Bạn có bất kỳ cải tiến nào khác có thể thực hiện cho trình tải lên này không? Hãy để lại một số gợi ý trong phần bình luận và nếu bạn đã thực hiện bất kỳ gợi ý nào ở trên, bạn cũng có thể chia sẻ công việc của mình trong phần bình luận.

Chúa phù hộ và chúc bạn viết mã vui vẻ!

Đọc thêm​

  • Cách tăng quy trình làm việc và giảm căng thẳng bằng âm thanh thiên nhiên
  • Kiểm tra sức khỏe kỹ thuật số
  • Thiết kế hình ảnh tùy chỉnh cho nội dung trực tuyến của bạn nhanh hơn!
  • Này các nhà thiết kế: Đừng là người suy nghĩ sau cùng nữa
 
Back
Bên trên