圖片上傳是 CMS、電商、社區的高頻功能。本文採用 jQuery Form Plugin 實現 iframe 級別的無刷新上傳(兼容 IE9+),結合後端 類型/大小/尺寸驗證、隨機重命名、縮略圖生成、JSON 回傳預覽 URL,構建生產可用的上傳組件。
<!-- 頁面頭部引入 --> <script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/jquery-form@4.3.0/dist/jquery.form.min.js"></script> <!-- 可選:圖片壓縮預覽 (客戶端縮放) --> <script src="https://cdn.jsdelivr.net/npm/browser-image-compression@2.0.1/dist/browser-image-compression.min.js"></script>
<div class="upload-box">
<input type="hidden" name="avatar" id="avatarUrl" value="">
<div class="preview" id="preview"><!-- 圖片預覽區 --></div>
<div class="upload-btn-wrap">
<span class="btn-text">上傳頭像</span>
<input type="file" name="avatar_file" id="avatarFile" accept="image/*" hidden>
</div>
<p class="tip" id="uploadTip"></p>
</div>
<script>
$(function() {
const $file = $('#avatarFile');
const $preview = $('#preview');
const $url = $('#avatarUrl');
const $tip = $('#uploadTip');
const $btnText = $('.btn-text');
// 點擊按鈕觸發文件選擇
$('.upload-btn-wrap').on('click', () => $file.trigger('click'));
// 文件選擇後自動提交
$file.on('change', function() {
if (!this.files.length) return;
const file = this.files[0];
// 客戶端預驗證
if (!/^image\//.test(file.type)) { $tip.text('請選擇圖片文件').css('color','red'); return; }
if (file.size > 5 * 1024 * 1024) { $tip.text('文件不能超過 5MB').css('color','red'); return; }
// 可選:客戶端壓縮 (大文件上傳前縮放)
// imageCompression(file, { maxSizeMB: 1, maxWidthOrHeight: 800, useWebWorker: true })
// .then(compressed => upload(compressed))
// .catch(console.error);
// return;
upload(file);
});
function upload(file) {
$tip.text('上傳中...').css('color','#666');
$btnText.text('上傳中...');
// 使用 jQuery Form Plugin 的 ajaxSubmit
$file.closest('form').length || $file.wrap('');
$file.parent().ajaxSubmit({
dataType: 'json',
beforeSubmit: () => {
$preview.empty();
$url.val('');
},
success: function(res) {
if (res.code === 200) {
$url.val(res.data.url);
$preview.html(`<?php
// api/upload_avatar.php
require_once '../db.php'; // PDO 連接
header('Content-Type: application/json; charset=utf-8');
// 1. 基礎檢查
if ($_SERVER['REQUEST_METHOD'] !== 'POST') exit(json_encode(['code'=>405,'msg'=>'Method Not Allowed']));
if (!isset($_FILES['avatar_file']) || $_FILES['avatar_file']['error'] !== UPLOAD_ERR_OK) {
exit(json_encode(['code'=>400,'msg'=>'未收到文件或上傳錯誤']));
}
$file = $_FILES['avatar_file'];
$allowedTypes = ['image/jpeg','image/png','image/gif','image/webp'];
$maxSize = 5 * 1024 * 1024; // 5MB
$maxDim = 2000; // 最大寬/高 2000px
// 2. 類型驗證(檢測真實 MIME,防偽裝)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if (!in_array($mime, $allowedTypes)) {
exit(json_encode(['code'=>400,'msg'=>'不支持的圖片格式,僅允許 JPG/PNG/GIF/WebP']));
}
if ($file['size'] > $maxSize) {
exit(json_encode(['code'=>400,'msg'=>'文件大小超過 5MB 限制']));
}
// 3. 尺寸驗證
list($w, $h) = getimagesize($file['tmp_name']);
if ($w > $maxDim || $h > $maxDim) {
exit(json_encode(['code'=>400,'msg'=>"圖片尺寸超過限制 ({$maxDim}px)"]));
}
// 4. 生成唯一存儲路徑
$ext = match($mime) {
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
};
$dateDir = date('Y/m/d');
$saveDir = __DIR__ . "/../uploads/avatars/{$dateDir}";
if (!is_dir($saveDir)) mkdir($saveDir, 0755, true);
$fileName = bin2hex(random_bytes(8)) . ".{$ext}";
$savePath = $saveDir . '/' . $fileName;
// 5. 移動文件(可選:生成縮略圖)
if (!move_uploaded_file($file['tmp_name'], $savePath)) {
exit(json_encode(['code'=>500,'msg'=>'文件保存失敗']));
}
// 可選:生成 120x120 縮略圖
$thumbPath = $saveDir . '/thumb_' . $fileName;
$src = imagecreatefromstring(file_get_contents($savePath));
$thumb = imagescale($src, 120, 120);
switch($ext) {
case 'jpg': imagejpeg($thumb, $thumbPath, 85); break;
case 'png': imagepng($thumb, $thumbPath, 8); break;
case 'webp': imagewebp($thumb, $thumbPath, 85); break;
}
imagedestroy($src); imagedestroy($thumb);
// 6. 返回可訪問 URL
$webPath = "/uploads/avatars/{$dateDir}/{$fileName}";
$thumbWebPath = "/uploads/avatars/{$dateDir}/thumb_{$fileName}";
echo json_encode([
'code' => 200,
'msg' => '上傳成功',
'data' => [
'url' => $webPath,
'thumb' => $thumbWebPath,
'width' => $w,
'height' => $h,
'size' => $file['size']
]
]);
| 環節 | 風險 | 對策 |
|---|---|---|
| 文件類型 | 偽裝 .php 為 .jpg | finfo_file() 檢測真實 MIME + 白名單 |
| 文件大小 | 佔滿磁盤/內存 | upload_max_filesize/post_max_size + 代碼二次校驗 |
| 文件名 | 路徑遍歷/覆蓋/注入 | random_bytes(8) 生成隨機名,僅保留驗證後的副檔名 |
| 存儲路徑 | Web 可直接訪問執行 | 上傳目錄配置 Nginx location ~ \.php$ { deny all; } 或存儲於非 Web 根目錄 |
| 圖片處理 | 解壓炸彈/畸形圖片導致 GD 崩潰 | 設置 ini_set('gd.jpeg_ignore_warning', 1)、限制最大像素 ini_set('memory_limit','256M') |
| 併發上傳 | 同名覆蓋 | 隨機文件名 + 日期目錄分散 |
// HTML5 Drag & Drop
const dropZone = document.querySelector('.upload-box');
['dragenter','dragover'].forEach(e => dropZone.addEventListener(e, e => { e.preventDefault(); dropZone.classList.add('dragover'); }));
['dragleave','drop'].forEach(e => dropZone.addEventListener(e, e => { e.preventDefault(); dropZone.classList.remove('dragover'); }));
dropZone.addEventListener('drop', e => {
e.preventDefault();
const files = e.dataTransfer.files;
if (files.length) handleFiles(files); // 复用上传逻辑
});
此方案以 jQuery Form Plugin 解決無刷新上傳兼容性,後端以 finfo + 尺寸檢查 + 隨機命名 + 非 Web 根目錄存儲 構建安全防線,前端提供 即時預覽、壓縮選項、拖拽支持,可直接作為項目通用上傳組件使用。