You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
}

错误原因分析

  1. 请求未发送:JavaScript代码中xhr.send(formData)被注释,导致FormData根本没有发送到后端,这是FormData为空的直接原因。
  2. Content-Type设置错误:手动设置了application/x-www-form-urlencoded,但FormData需要浏览器自动设置multipart/form-data(包含boundary分隔符),手动设置会导致后端无法解析FormData。
  3. Image对象使用不当:通过Image对象的src获取dataURL添加到FormData,Image加载是异步操作,直接使用可能导致数据未准备好;同时FormData更适合传入Blob/File对象而非字符串形式的dataURL。
  4. 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]);
}

验证步骤

  1. 启动Laravel项目,确保public磁盘配置正确(默认已配置,可在config/filesystems.php查看)。
  2. 前端页面打开摄像头,拍照后点击上传,查看浏览器控制台是否输出"上传成功"。
  3. 检查storage/app/public/profile_images目录下是否生成了对应的图片文件。
  4. 访问storage/profile_images/[文件名](需先执行php artisan storage:link创建软链),确认图片可正常访问。

内容的提问来源于stack exchange,提问作者scorpions78

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 12:55:26