Chào các bạn! Dropzone.js là một thư viện nhẹ và dễ sử dụng cho phép bạn tạo khu vực kéo và thả tệp tin. Nó tự động xử lý các tác vụ phức tạp như xem trước hình ảnh, hiển thị tiến độ tải lên, và xử lý lỗi. Với Dropzone.js, bạn có thể biến một form upload tệp tin thông thường thành một trải nghiệm tương tác và thân thiện với người dùng.

Trong bài viết này, CodeTutHub sẽ hướng dẫn bạn cách sử dụng Laravel 11 (bài viết sử dụng Laravel 11 để hướng dẫn, tuy nhiên bạn cũng có thể dựa trên code cho các version khác) để tạo chức năng upload nhiều ảnh cùng lúc với Dropzone.js, một thư viện JavaScript phổ biến và mạnh mẽ này.

Các bước triển khai:

Chúng ta sẽ thực hiện các bước sau:

  • Cài đặt Laravel 11: Khởi tạo một dự án Laravel mới.
  • Cài đặt Dropzone.js: Thêm thư viện Dropzone.js vào dự án.
  • Tạo migration và model: Lưu dữ liệu file đã được upload
  • Thiết lập Route: Định nghĩa các route cần thiết.
  • Tạo Controller: Xử lý logic tải lên tệp tin.
  • Tạo View: Xây dựng giao diện người dùng với Dropzone.
  • Xử lý lưu trữ tệp tin: Cấu hình và lưu trữ ảnh đã tải lên.
  • Hiển thị ảnh đã tải lên (tùy chọn): Cách hiển thị các ảnh sau khi tải lên thành công.

1. Cài đặt Laravel (nếu chưa có)

Đầu tiên, nếu bạn chưa có sẵn project laravel hãy tạo một dự án Laravel mới bằng Composer:

shell
composer create-project laravel/laravel laravel-demo
cd laravel-demo

Phiên bản mới nhất của Laravel hiện tại là 12, mời bạn xem thêm bài viết về Có gì mới trong Laravel 12?

2. Cài đặt Dropzone.js

Có nhiều cách để thêm Dropzone.js vào dự án của bạn. Cách đơn giản nhất là sử dụng CDN. Thêm các liên kết sau vào phần <head> của layout Blade của bạn (ví dụ: resources/views/layouts/app.blade.php hoặc trực tiếp vào file view mà bạn sẽ sử dụng Dropzone):

html
<script src="https://unpkg.com/dropzone@6.0.0-beta.1/dist/dropzone-min.js"></script>
<link href="https://unpkg.com/dropzone@6.0.0-beta.1/dist/dropzone.css" rel="stylesheet" type="text/css"/>

Nếu bạn muốn cài đặt thông qua npm (được khuyến nghị cho các dự án lớn hơn để quản lý tài nguyên):

shell
npm install dropzone

Sau đó, bạn cần biên dịch các tài nguyên frontend của mình bằng Vite. Trong file resources/js/app.js của bạn, thêm:

js
import Dropzone from 'dropzone';
window.Dropzone = Dropzone;

Và đảm bảo rằng Vite đang chạy để biên dịch tài nguyên:

shell
npm run dev

3. Tạo Migration và Model

Tạo migration để lưu thông tin ảnh trong database:

shell
php artisan make:model Image -m

Chỉnh sửa file migration vừa tạo:

php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('images', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('filesize');
            $table->string('path');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('images');
    }
};

Chỉnh sửa model Image:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Image extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
        'filesize',
        'path'
    ];
}

Chạy migration:

shell
php artisan migrate

4. Tạo Route

Chúng ta cần một route để hiển thị form upload và một route để xử lý yêu cầu upload ảnh. Mở file routes/web.php và thêm:

php
<?php

use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});


Route::get('upload', [\App\Http\Controllers\UploadFileController::class, 'index']);
Route::post('upload/store', [\App\Http\Controllers\UploadFileController::class, 'store'])->name('upload.store');

5. Tạo Controller

Tạo controller để xử lý upload:

shell
php artisan make:controller UploadFileController

Chỉnh sửa Controller:

php
app/Http/Controllers/UploadFileController.php
<?php

namespace App\Http\Controllers;

use App\Models\Image;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\View\View;

class UploadFileController extends Controller
{
    /**
     * @return View
     */
    public function index(): View
    {
        $images = Image::all();
        return view('upload.index', compact('images'));
    }

    /**
     * @param Request $request
     * @return JsonResponse
     */
    public function store(Request $request): JsonResponse
    {
        // Initialize an array to store image information
        $images = [];

        // Process each uploaded image
        foreach ($request->file('files') as $image) {
            // Generate a unique name for the image
            $imageName = time() . '_' . Str::random() . '.' . $image->getClientOriginalExtension();

            // Store the image in the storage/app/public/images directory
            $path = $image->storeAs('images', $imageName, 'public');

            // Get the public URL for the stored image
            $url = Storage::url($path);

            // Add image details to the array
            $images[] = [
                'name' => $imageName,
                'path' => $url,
                'filesize' => Storage::disk('public')->size($path),
            ];
        }

        // Store images in the database
        foreach ($images as $imageData) {
            Image::create($imageData);
        }

        return response()->json(['success' => $images]);
    }
}

Lưu ý:

  • Trong ví dụ này, chúng ta lưu ảnh vào thư mục public/images. Điều này giúp dễ dàng truy cập ảnh qua URL.
  • Bạn cần tạo symbolic link: php artisan storage:link. Sau đó, sử dụng $path = $image->storeAs('images', $imageName, 'public');
  • Hãy đảm bảo thư mục storage/app/public/images có quyền ghi.

6. Tạo View với Dropzone.js

Tạo file resources/views/upload/index.blade.php:

php
resources/views/upload/index.blade.php
<!DOCTYPE html>
<html>
<head>
    <title>Laravel 11 Drag and Drop File Upload with Dropzone JS - CodeTutHub</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
          crossorigin="anonymous">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
    <script src="https://unpkg.com/dropzone@6.0.0-beta.1/dist/dropzone-min.js"></script>
    <link href="https://unpkg.com/dropzone@6.0.0-beta.1/dist/dropzone.css" rel="stylesheet" type="text/css"/>
    <style type="text/css">
        .dz-preview .dz-image img {
            width: 100% !important;
            height: 100% !important;
            object-fit: cover;
        }
    </style>
</head>
<body>

<div class="container">
    <div class="card mt-5">
        <h3 class="card-header p-3">Laravel 11 Drag and Drop File Upload with Dropzone JS - codetuthub.com</h3>
        <div class="card-body">
            <form action="{{ route('upload.store') }}" method="post" enctype="multipart/form-data"
                  id="uploadForm"
                  class="dropzone">
                @csrf
                <div>
                    <h4>Upload multiple image by click on box</h4>
                </div>
            </form>
            <button id="uploadFile" class="btn btn-success mt-1">Upload Images</button>
        </div>
    </div>
</div>

<script type="text/javascript">

    Dropzone.autoDiscover = false;

    var images = {{ Js::from($images) }};

    const dropzone = new Dropzone('#uploadForm', {
        autoProcessQueue: false,
        paramName: 'files',
        uploadMultiple: true,
        parallelUploads: 5, // 5 files sent per batch (2 is default)
        maxFilesize: 5,
        acceptedFiles: '.jpeg,.jpg,.png,.gif',
        init: function() {
            const dropzoneInstance = this;
            $.each(images, function(key, value) {
                var mockFile = {name: value.name, size: value.filesize};
                dropzoneInstance.emit('addedfile', mockFile);
                dropzoneInstance.emit('thumbnail', mockFile, value.path);
                dropzoneInstance.emit('complete', mockFile);
            });
        },
    });

    $('#uploadFile').click(function() {
        dropzone.processQueue();
    });
    $('#uploadForm').submit(function(e) { e.preventDefault(); });

</script>

</body>
</html>

Giải thích mã JavaScript:

  • Dropzone.autoDiscover = false;: Ngăn Dropzone tự động tìm kiếm các form có class dropzone và tự động khởi tạo. Chúng ta sẽ khởi tạo thủ công.
  • new Dropzone("#uploadForm", { ... });: Khởi tạo một đối tượng Dropzone, liên kết với form có id="uploadForm".
  • action="{{ route('upload.store') }}" Chỉ định URL mà Dropzone sẽ gửi yêu cầu tải lên.
  • paramName: "files": Dropzone sẽ gửi tệp tin dưới tên files trong yêu cầu POST. Đảm bảo tên này khớp với tên bạn sử dụng trong Controller ($request->hasFile('files')).
  • maxFilesize: Kích thước tệp tối đa được phép (tính bằng MB).
  • acceptedFiles: Các định dạng tệp được chấp nhận.
  • addRemoveButton: true: Cho phép hiển thị nút "Xóa ảnh" trên mỗi ảnh đã tải lên.
  • dictRemoveFile, dictDefaultMessage, v.v.: Các tùy chỉnh văn bản hiển thị.
  • init: function() { ... }: Chứa các sự kiện (event listeners) của Dropzone.
    • this.on("success", function(file, response) { ... });: Được kích hoạt khi một tệp được tải lên thành công. response chứa dữ liệu trả về từ server (trong trường hợp này là tên file).
    • this.on("error", function(file, message) { ... });: Được kích hoạt khi có lỗi trong quá trình tải lên.
    • this.on("removedfile", function(file) { ... });: Được kích hoạt khi người dùng nhấn nút xóa trên một ảnh.

7. Tạo Storage Link

Chạy lệnh sau để liên kết thư mục lưu trữ ảnh:

shell
php artisan storage:link

8. Khởi động Laravel App

Chạy ứng dụng Laravel của bạn:

shell
php artisan serve

Mở trình duyệt và truy cập: http://127.0.0.1:8000/upload Bạn sẽ thấy giao diện kéo và thả ảnh. Hãy thử kéo một vài ảnh vào hoặc nhấp vào khu vực để chọn ảnh.

Click vào ô "Drop files here to upload" để chọn nhiều file, hoặc kéo thả ảnh vào và sau đó Click "Upload Images"

Files sẽ được lưu trong folder:

Và trong cơ sở dữ liệu:

9. Cấu hình và Bảo mật

  • Kích thước tệp tin tối đa: Ngoài maxFilesize của Dropzone, bạn cũng nên cấu hình kích thước tệp tin tối đa ở phía server trong file php.ini:
    • upload_max_filesize
    • post_max_size
  • Kiểm tra loại tệp tin: Luôn luôn kiểm tra loại tệp tin ở phía server (như đã làm trong Controller với acceptedFiles) để ngăn chặn việc tải lên các tệp độc hại.
  • Đổi tên tệp tin: Để tránh trùng lặp tên tệp và các vấn đề bảo mật, hãy đổi tên tệp tin khi lưu trữ (ví dụ: sử dụng time() hoặc Str::random() kèm với tên gốc).
  • Đường dẫn lưu trữ: Đối với các ứng dụng production, nên lưu trữ ảnh vào thư mục storage/app/public và tạo symbolic link để truy cập công khai, thay vì lưu trực tiếp vào public. Điều này giúp kiểm soát quyền truy cập tốt hơn.
  • Xóa ảnh: Nếu bạn cho phép người dùng xóa ảnh, hãy triển khai chức năng xóa ảnh trên server để loại bỏ tệp tin vật lý.

10. Nâng cao (optional)

  • Tiến độ tải lên toàn bộ: Dropzone có các sự kiện để theo dõi tiến độ tải lên tổng thể.
  • Tùy chỉnh giao diện: Bạn có thể tùy chỉnh hoàn toàn giao diện của Dropzone bằng CSS.
  • Tích hợp với cơ sở dữ liệu: Sau khi tải lên thành công, bạn có thể lưu trữ thông tin ảnh (như tên file, đường dẫn, ID người dùng) vào cơ sở dữ liệu để dễ dàng quản lý.
  • Validation server-side: Thêm các quy tắc validation chặt chẽ hơn trong Laravel Controller để kiểm tra kích thước, loại tệp, v.v.

Kết luận

Dropzone.js là một công cụ mạnh mẽ và dễ sử dụng để triển khai tính năng upload nhiều ảnh với giao diện kéo và thả thân thiện. Bằng cách làm theo hướng dẫn này, bạn đã có thể tích hợp thành công Dropzone.js vào ứng dụng Laravel của mình, nâng cao trải nghiệm người dùng và giúp việc tải lên tệp tin trở nên dễ dàng hơn bao giờ hết. Bạn có thể lấy toàn bộ mã nguồn ví dụ từ repository GitHub dưới đây:

https://github.com/vantoantg/laravel-11-demo/tree/feature/drag-and-drop-file-upload-with-dropzone-js

Chúc bạn thành công!