Laravel摄像头拍照上传服务器:FormData为空问题排查求助
摄像头拍照上传Laravel时FormData为空的问题排查与解决
问题描述
尝试调用摄像头拍照并上传至Laravel项目的文件系统,但遇到FormData为空的问题,无法完成上传。以下是相关代码:
HTML代码
<div class="form-group"> <label for="videoSelect">Camera</label> <div class="camera"> <select id="videoSelect" class="custom-select"></select> <button id="startCameraButton" class="btn btn-success">Start Camera</button> <video id="video">Video stream not available.</video> <button id="takePictureButton" class="btn btn-info d-none">Take photo</button> </div> <canvas class="d-none" id="canvas" name="fileName"></canvas> </div>
JavaScript代码
document.getElementById("editProfile").addEventListener("click", function(event){ event.preventDefault() }); var width = 320; var height = 0; var streaming = false; var localstream = null; startCameraButton.onclick = start; takePictureButton.onclick = takepicture; navigator.mediaDevices.enumerateDevices() .then(gotDevices) .catch(function (err) { console.log("An error occured while getting device list! " + err); }); function gotDevices(deviceInfos) { while (videoSelect.firstChild) { videoSelect.removeChild(videoSelect.firstChild); } for (var i = 0; i !== deviceInfos.length; ++i) { var deviceInfo = deviceInfos[i]; var option = document.createElement('option'); option.value = deviceInfo.deviceId; if (deviceInfo.kind === 'videoinput') { option.text = deviceInfo.label || 'Camera ' + (videoSelect.length + 1); videoSelect.appendChild(option); } } } function start() { takePictureButton.classList.remove("d-none") stopVideo(); clearphoto(); var videoSource = videoSelect.value; var constraints = { audio: false, video: {deviceId: videoSource ? {exact: videoSource} : undefined} }; navigator.mediaDevices.getUserMedia(constraints). then(gotStream).then(gotDevices).catch(handleError); } function gotStream(stream) { localstream = stream; video.srcObject = stream; video.play(); // Refresh button list in case labels have become available return navigator.mediaDevices.enumerateDevices(); } function handleError(error) { console.log('navigator.getUserMedia error: ', error); } video.addEventListener('canplay', function (ev) { if (!streaming) { height = video.videoHeight / (video.videoWidth / width); video.setAttribute('width', width); video.setAttribute('height', height); canvas.setAttribute('width', width); canvas.setAttribute('height', height); streaming = true; } }, false); clearphoto(); function clearphoto() { var context = canvas.getContext('2d'); context.fillStyle = "#AAA"; context.fillRect(0, 0, canvas.width, canvas.height); } function takepicture() { canvas.classList.remove("d-none") var context = canvas.getContext('2d'); if (width && height) { canvas.width = width; canvas.height = height; context.drawImage(video, 0, 0, width, height); var dataURL = canvas.toDataURL("image/jpeg", 0.95); if (dataURL && dataURL != "data:,") { var fileName = generateImageName(); uploadimage(dataURL, fileName); } else { console.log("Image not available"); } } else { clearphoto(); } } function generateImageName() { var today = new Date(); var date = today.getFullYear()+'-'+(today.getMonth()+1)+'-'+today.getDate(); var time = today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds(); var dateTime = date+'_'+time; let name = document.getElementById("inputName").value; return name+"_"+dateTime+ ".jpg"; } function uploadimage(dataurl, filename) { let image = new Image(); image.src = dataurl; var xhr = new XMLHttpRequest(); xhr.onload = function () { if (xhr.status >= 200 && xhr.status < 300) { console.log('¡Éxito!', xhr.response); }else{ console.log('Error en la petición!'); } } xhr.open('POST', "{{ route('patients.profileImage') }}"); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.setRequestHeader("X-CSRF-Token", "{{ csrf_token() }}"); var formData = new FormData(); formData.append("image", image.src); formData.append("fileName", filename); //xhr.send(formData); } function stopVideo() { if (localstream) { localstream.getTracks().forEach(function (track) { track.stop(); localstream = null; }); } }
Controller代码
function profileImage(Request $request) { $img = $request->get('image'); $imgTran1 = str_replace('data:image/jpg;base64,', '', $img); $imgTran2 = str_replace(' ', '+', $imgTran1); $data = base64_decode($imgTran2); \Storage::disk('public')->move($img, $data); }
错误原因分析
- 请求未发送:JavaScript代码中
xhr.send(formData)被注释,导致FormData根本没有发送到后端,这是FormData为空的直接原因。 - Content-Type设置错误:手动设置了
application/x-www-form-urlencoded,但FormData需要浏览器自动设置multipart/form-data(包含boundary分隔符),手动设置会导致后端无法解析FormData。 - Image对象使用不当:通过Image对象的
src获取dataURL添加到FormData,Image加载是异步操作,直接使用可能导致数据未准备好;同时FormData更适合传入Blob/File对象而非字符串形式的dataURL。 - Laravel存储方法错误:使用
Storage::move()方法,该方法用于移动已存在的文件,而非写入base64解码后的二进制数据,应该用put()方法。
修复方案
1. 修正JavaScript代码
主要修改uploadimage函数,取消请求发送注释,删除错误的Content-Type设置,将dataURL转换为Blob后传入FormData:
function uploadimage(dataurl, filename) { // 将dataURL转换为Blob对象 const blob = dataURLtoBlob(dataurl); var xhr = new XMLHttpRequest(); xhr.onload = function () { if (xhr.status >= 200 && xhr.status < 300) { console.log('上传成功!', xhr.response); } else { console.log('请求出错!'); } } xhr.open('POST', "{{ route('patients.profileImage') }}"); xhr.setRequestHeader("X-CSRF-Token", "{{ csrf_token() }}"); // 移除手动设置的Content-Type,浏览器会自动处理multipart/form-data var formData = new FormData(); // 将Blob添加到FormData,第三个参数指定文件名 formData.append("image", blob, filename); xhr.send(formData); // 取消注释,发送请求 } // 新增dataURL转Blob的工具函数 function dataURLtoBlob(dataurl) { var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); while(n--){ u8arr[n] = bstr.charCodeAt(n); } return new Blob([u8arr], {type: mime}); }
额外优化:generateImageName中的时间包含冒号,部分文件系统不支持文件名含冒号,建议替换为下划线:
function generateImageName() { var today = new Date(); var date = today.getFullYear()+'-'+(today.getMonth()+1)+'-'+today.getDate(); var time = today.getHours() + "_" + today.getMinutes() + "_" + today.getSeconds(); // 冒号改下划线 var dateTime = date+'_'+time; let name = document.getElementById("inputName").value || "profile"; // 添加默认值避免空文件名 return name+"_"+dateTime+ ".jpg"; }
2. 修正Laravel Controller代码
现在前端传的是Blob文件,后端可以直接通过$request->file()获取并存储:
use Illuminate\Http\Request; use Illuminate\Support\Facades\Storage; function profileImage(Request $request) { // 验证是否有文件上传 if (!$request->hasFile('image')) { return response()->json(['status' => 'error', 'message' => '未上传图片'], 400); } $imageFile = $request->file('image'); // 验证文件类型 if (!$imageFile->isValid() || !in_array($imageFile->getMimeType(), ['image/jpeg', 'image/png'])) { return response()->json(['status' => 'error', 'message' => '无效的图片文件'], 400); } $filename = $imageFile->getClientOriginalName(); // 存储到public磁盘的profile_images目录下 Storage::disk('public')->putFileAs('profile_images', $imageFile, $filename); return response()->json(['status' => 'success', 'path' => 'storage/profile_images/' . $filename]); }
如果仍希望使用dataURL字符串上传(不转Blob),Controller修改如下:
function profileImage(Request $request) { $imgData = $request->get('image'); $filename = $request->get('fileName'); if (!$imgData || !$filename) { return response()->json(['status' => 'error', 'message' => '图片数据或文件名缺失'], 400); } // 注意dataURL是image/jpeg,替换对应前缀 $imgTran1 = str_replace('data:image/jpeg;base64,', '', $imgData); $imgTran2 = str_replace(' ', '+', $imgTran1); $imageContent = base64_decode($imgTran2); if (!$imageContent) { return response()->json(['status' => 'error', 'message' => '图片解码失败'], 400); } // 使用put方法写入二进制内容 Storage::disk('public')->put('profile_images/' . $filename, $imageContent); return response()->json(['status' => 'success', 'path' => 'storage/profile_images/' . $filename]); }
验证步骤
- 启动Laravel项目,确保
public磁盘配置正确(默认已配置,可在config/filesystems.php查看)。 - 前端页面打开摄像头,拍照后点击上传,查看浏览器控制台是否输出"上传成功"。
- 检查
storage/app/public/profile_images目录下是否生成了对应的图片文件。 - 访问
storage/profile_images/[文件名](需先执行php artisan storage:link创建软链),确认图片可正常访问。
内容的提问来源于stack exchange,提问作者scorpions78
相关产品推荐
相关产品推荐

