Đẩy mạnh trải nghiệm người dùng với hệ thống tải lên nhiều ảnh không làm mới trang bằng PHP, jQuery và AJAX

Trong các ứng dụng web hiện đại, việc tải lên nhiều tệp hình ảnh một cách mượt mà — mà không gây gián đoạn giao diện — đã trở thành tiêu chuẩn thiết yếu. Giải pháp dưới đây xây dựng một hệ thống tải lên đa ảnh hoàn chỉnh dựa trên ba thành phần cốt lõi: HTML5 <input type="file" multiple>, thư viện jQuery để xử lý sự kiện và gửi yêu cầu, cùng logic phía máy chủ bằng PHP đảm bảo tính an toàn và độ tin cậy.

Khởi tạo giao diện người dùng

Bắt đầu từ phần tử đầu vào đơn giản nhưng mạnh mẽ:

<input 
  type="file" 
  id="imageSelector" 
  name="batchImages[]" 
  multiple 
  accept="image/jpeg,image/png,image/webp">
<div id="previewContainer"></div>
<button id="uploadTrigger">Tải lên tất cả</button>

Thuộc tính multiple cho phép chọn đồng thời nhiều tệp, trong khi accept giúp bộ chọn tệp hiển thị đúng định dạng hỗ trợ — tuy nhiên đây chỉ là gợi ý phía trình duyệt, không phải cơ chế kiểm soát an toàn.

Xử lý danh sách tệp tại phía client

Khi người dùng chọn tệp, sự kiện change được kích hoạt. Đối tượng FileList trả về không phải mảng thật, nên cần chuyển đổi trước khi xử lý:

$('#imageSelector').on('change', function(e) {
  const selectedFiles = e.target.files;
  
  // Chuyển sang mảng thực để sử dụng phương thức như filter(), map()
  const validImages = Array.from(selectedFiles)
    .filter(file => file.type.startsWith('image/') && file.size <= 5 * 1024 * 1024)
    .slice(0, 12); // Giới hạn tối đa 12 ảnh

  renderPreviews(validImages);
});

Hàm renderPreviews() sử dụng FileReader để đọc từng tệp dưới dạng dataURL, sau đó chèn vào vùng xem trước:

function renderPreviews(files) {
  const container = $('#previewContainer');
  container.empty();
  
  files.forEach(file => {
    const reader = new FileReader();
    reader.onload = () => {
      const previewItem = $(
        '<div class="thumbnail">' +
          '<img src="' + reader.result + '" alt="' + file.name + '">' +
          '<span class="filename">' + file.name.substring(0, 20) + '...</span>' +
        '</div>'
      );
      container.append(previewItem);
    };
    reader.readAsDataURL(file);
  });
}

Gói dữ liệu và gửi yêu cầu không đồng bộ

Để truyền tệp qua AJAX, cần sử dụng FormData. Cấu hình jQuery phải được điều chỉnh kỹ lưỡng để tránh lỗi phổ biến:

$('#uploadTrigger').on('click', function() {
  const files = $('#imageSelector')[0].files;
  if (files.length === 0) return;

  const payload = new FormData();
  Array.from(files).forEach(file => {
    payload.append('photos[]', file);
  });

  $.ajax({
    url: '/api/handle-upload.php',
    method: 'POST',
    data: payload,
    contentType: false,      // Bắt buộc: để trình duyệt tự sinh boundary
    processData: false,      // Bắt buộc: giữ nguyên dữ liệu nhị phân
    cache: false,            // Tránh cache trên IE cũ
    xhr: function() {
      const xhr = new window.XMLHttpRequest();
      xhr.upload.addEventListener('progress', handleUploadProgress);
      return xhr;
    }
  }).done(handleUploadSuccess).fail(handleUploadError);
});

Xử lý phản hồi tiến trình tải lên

Để cung cấp phản hồi tức thì, gắn listener vào sự kiện progress của đối tượng xhr.upload:

function handleUploadProgress(e) {
  if (e.lengthComputable) {
    const percent = Math.round((e.loaded / e.total) * 100);
    $('.progress-bar').css('width', percent + '%').text(percent + '%');
  }
}

Xử lý phía máy chủ bằng PHP

Tập tin handle-upload.php nhận dữ liệu từ $_FILES. Lưu ý cấu trúc mảng hai chiều khi gửi nhiều tệp:

<?php
header('Content-Type: application/json; charset=utf-8');

$uploadDir = __DIR__ . '/storage/uploads/';
$allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
$maxFileSize = 5 * 1024 * 1024;
$response = ['success' => false, 'uploaded' => []];

if ($_SERVER['REQUEST_METHOD'] !== 'POST' || empty($_FILES['photos'])) {
  http_response_code(400);
  echo json_encode(['error' => 'Dữ liệu không hợp lệ']);
  exit;
}

$files = $_FILES['photos'];
$fileCount = count($files['name']);

for ($i = 0; $i < $fileCount; $i++) {
  $tmpPath = $files['tmp_name'][$i];
  $originalName = $files['name'][$i];
  $mimeType = $files['type'][$i];
  $size = $files['size'][$i];
  $error = $files['error'][$i];

  if ($error !== UPLOAD_ERR_OK) {
    continue;
  }

  // Kiểm tra kích thước
  if ($size > $maxFileSize) {
    continue;
  }

  // Xác minh MIME thực tế bằng finfo
  $finfo = finfo_open(FILEINFO_MIME_TYPE);
  $realType = finfo_file($finfo, $tmpPath);
  finfo_close($finfo);

  if (!in_array($realType, $allowedTypes)) {
    continue;
  }

  // Tạo đường dẫn lưu trữ theo ngày
  $dateDir = date('Y/m/d');
  $uploadPath = $uploadDir . $dateDir . '/';
  if (!is_dir($uploadPath)) {
    mkdir($uploadPath, 0755, true);
  }

  // Đặt tên an toàn dựa trên hash nội dung
  $hash = hash_file('sha256', $tmpPath);
  $ext = match($realType) {
    'image/jpeg' => 'jpg',
    'image/png'  => 'png',
    'image/webp' => 'webp',
    default      => 'jpg'
  };
  $finalPath = $uploadPath . $hash . '.' . $ext;

  // Di chuyển tệp một cách an toàn
  if (is_uploaded_file($tmpPath) && move_uploaded_file($tmpPath, $finalPath)) {
    $response['uploaded'][] = [
      'url' => '/storage/uploads/' . $dateDir . '/' . $hash . '.' . $ext,
      'size' => $size,
      'name' => $originalName
    ];
  }
}

$response['success'] = !empty($response['uploaded']);
echo json_encode($response, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);

Bảo mật nâng cao

  • Không bao giờ tin tưởng giá trị $_FILES['type'] — luôn xác minh lại bằng finfo.
  • Thiết lập quyền thư mục lưu trữ ở mức 0755 và vô hiệu hóa thực thi script PHP bên trong thư mục upload qua cấu hình web server.
  • Sử dụng tên tệp dựa trên hash nội dung thay vì tên gốc nhằm ngăn chặn xung đột và lộ thông tin nhạy cảm.
  • Giới hạn kích thước tệp cả ở phía client lẫn server (upload_max_filesize, post_max_size trong php.ini).

Hiển thị kết quả và xử lý lỗi

Phía client phân tích phản hồi JSON để cập nhật giao diện:

function handleUploadSuccess(data) {
  if (data.success && data.uploaded.length) {
    data.uploaded.forEach(item => {
      const card = $(
        '<div class="uploaded-card">' +
          '<img src="' + item.url + '">' +
          '<p>' + item.name + '</p>' +
        '</div>'
      );
      $('#gallery').append(card);
    });
  }
}

function handleUploadError(xhr) {
  const status = xhr.status || 0;
  const messages = {
    0: 'Không thể kết nối đến máy chủ.',
    400: 'Yêu cầu không hợp lệ.',
    413: 'Tệp quá lớn.',
    500: 'Lỗi hệ thống. Vui lòng thử lại sau.'
  };
  alert(messages[status] || 'Đã xảy ra lỗi không xác định.');
}

Thẻ: php jQuery ajax file-upload web-security

Đăng vào ngày 16 tháng 7 lúc 12:20