如何使用 React.js + Django Rest Framework 保存带有表单提交的 blob 文件

Posted

技术标签:

【中文标题】如何使用 React.js + Django Rest Framework 保存带有表单提交的 blob 文件【英文标题】:How Can I save a blob file with a form submission using React.js + Django Rest Framework 【发布时间】:2020-09-17 19:59:10 【问题描述】:

我正在尝试使用 react-image-crop 提交在 react 应用程序中生成的裁剪图像,并使用 Axios 将其保存到 Django Rest Api。

该应用在前端使用 React、Redux 和 Axios,在后端使用 Django Rest Framework。

表单在没有文件的情况下可以正常提交,并且在没有添加文件代码的情况下保存在 django 中。

现在文件已添加到表单提交中,服务器返回 400 错误。

我怀疑我没有以正确的格式将 blob 提交到 django 服务器,但我不确定如何继续。

更新:我在下面使用 axios 将 blob url 转换为 blob,现在我正在尝试一个可以提交给 django rest api 的文件。表单在没有文件的情况下提交给 django rest API,但是当将文件添加到表单提交中时,我收到 400 错误。我已经更新了代码以反映我最新的集成。我已经包含了将标头设置为 multipart/form-data 的代码。错误似乎是在下面的onSubmit()方法中的文件转换过程中。

这是我的相关代码: 导入 react-image-crop 库。

// Cropper
import 'react-image-crop/dist/ReactCrop.css';
import ReactCrop from 'react-image-crop';

反应钩子内部的功能:

const AdCreator = ( addFBFeedAd ) => 
  const [title, setTitle] = useState('');
  const [headline, setHeadline] = useState('');
  const [ad_text, setAdText] = useState('');
  const cropper = useRef();



  // Cropper
  const [upImg, setUpImg] = useState();
  const imgRef = useRef(null);
  const [crop, setCrop] = useState( unit: '%', width: 30, aspect: 1.91 / 1 );
  const [previewUrl, setPreviewUrl] = useState();

  const onSelectFile = e => 
    if (e.target.files && e.target.files.length > 0) 
      const reader = new FileReader();
      reader.addEventListener('load', () => setUpImg(reader.result));
      reader.readAsDataURL(e.target.files[0]);
    
  ;

  const onLoad = useCallback(img => 
    imgRef.current = img;
  , []);

  const makeClientCrop = async crop => 
    if (imgRef.current && crop.width && crop.height) 
      createCropPreview(imgRef.current, crop, 'newFile.jpeg');
    
  ;
  const makePostCrop = async crop => 
    if (imgRef.current && crop.width && crop.height) 
      createCropPreview(imgRef.current, crop, 'newFile.jpeg');
    
  ;

  const createCropPreview = async (image, crop, fileName) => 
    const canvas = document.createElement('canvas');
    const scaleX = image.naturalWidth / image.width;
    const scaleY = image.naturalHeight / image.height;
    canvas.width = crop.width;
    canvas.height = crop.height;
    const ctx = canvas.getContext('2d');

    ctx.drawImage(
      image,
      crop.x * scaleX,
      crop.y * scaleY,
      crop.width * scaleX,
      crop.height * scaleY,
      0,
      0,
      crop.width,
      crop.height
    );

    return new Promise((resolve, reject) => 
      canvas.toBlob(blob => 
        if (!blob) 
          reject(new Error('Canvas is empty'));
          return;
        
        blob.name = fileName;
        window.URL.revokeObjectURL(previewUrl);
        setPreviewUrl(window.URL.createObjectURL(blob));
      , 'image/jpeg');
    );
 ;

  const onSubmit = (e) => 
    e.preventDefault();
    const config =  responseType: 'blob' ;
    let file = axios.get(previewUrl, config).then(response => 
        new File([response.data], title, type:"image/jpg", lastModified:new Date());       
    ); 
    let formData = new FormData();
    formData.append('title', title);
    formData.append('headline', headline);
    formData.append('ad_text', ad_text);
    formData.append('file', file);
    addFBFeedAd(formData);


  ;
  return (

表单部分:

<form method="post" id='uploadForm'>                  
          <div className="input-field">
            <label for="id_file">Upload Your Image</label>
            <br/>
            /* form.file */
          </div>
          <div>
            <div>
              <input type="file" accept="image/*" onChange=onSelectFile />
            </div>
            <ReactCrop
              src=upImg
              onImageLoaded=onLoad
              crop=crop
              onChange=c => setCrop(c)
              onComplete=makeClientCrop
              ref=cropper
            />
            previewUrl && <img  src=previewUrl />
          </div>

            <button className="btn darken-2 white-text btn-large teal btn-extend" id='savePhoto' onClick=onSubmit value="Save Ad">Save Ad</button>

        </form>

这是 Axios 调用:

 export const addFBFeedAd = (fbFeedAd) => (dispatch, getState) => 
  setLoading();
  axios
    .post(`http://localhost:8000/api/fb-feed-ads/`, fbFeedAd, tokenMultiPartConfig(getState))
    .then((res) => 
      dispatch(createMessage( addFBFeedAd: 'Ad Added' ));
      dispatch(
        type: SAVE_AD,
        payload: res,
      );
    )
    .catch((err) => dispatch(returnErrors(err)));

这是我将标题设置为多部分表单数据的地方

export const tokenMultiPartConfig = (getState) => 
 // Get token from state
  const token = getState().auth.token;

  // Headers
  const config = 
    headers: 
      "Content-type": "multipart/form-data",
    ,
  ;

  // If token, add to headers config
  if (token) 
    config.headers['Authorization'] = `Token $token`;
  

  return config;
;

模型:

class FB_Feed_Ad(models.Model):
    title = models.CharField(max_length=100, blank=True)
    headline = models.CharField(max_length=25, blank=True)
    ad_text = models.CharField(max_length=125, blank=True)
    file = models.ImageField(upload_to='photos/%Y/%m/%d/', blank=True)

裁剪预览 blob:

blob:http://localhost:3000/27bb58e5-4d90-481d-86ab-7baa717cc023

我在 axios 调用后控制台记录了裁剪的图像。

File:  
Promise <pending>
__proto__: Promise
[[PromiseStatus]]: "resolved"
[[PromiseValue]]: undefined
AdCreator.js:169 formData: 
FormData 
__proto__: FormData

如您所见,我正在尝试提交由 react-image-cropper 生成的 blob 图像文件,作为提交表单时表单数据的一部分。我想将裁剪后的图像保存到 Django Rest API。 有什么建议吗?

【问题讨论】:

更新:我已经使用下面的 axios 将 blob url 转换为 blob,现在我正在尝试一个可以提交给 django rest api 的文件。表单在没有文件的情况下提交给 django rest API,但是当将文件添加到表单提交中时,我收到 400 错误。我已经更新了代码以反映我最新的集成。我已经包含了将标头设置为 multipart/form-data 的代码。错误似乎是在下面的onSubmit()方法中的文件转换过程中。 我最近发布了medium.com/swlh/adding-crop-before-upload-in-react-22dfcf3a95b7,它展示了如何结合 react-image-crop 和 react-uploady 轻松将裁剪后的图像上传到服务器。 【参考方案1】:

您应该将其作为“Content-Type”:“multipart/form-data”发送到 django imageField。所以你应该适当地转换你的 blob 文件:

let cropImg = this.$refs.cropper.getCroppedCanvas().toDataURL();
let arr = this.cropImg.split(","),
    mime = arr[0].match(/:(.*?);/)[1],
    bstr = atob(arr[1]),
    n = bstr.length,
    u8arr = new Uint8Array(n);

while (n--) 
    u8arr[n] = bstr.charCodeAt(n);


let imageCrop = new File([u8arr], 'imagename',  type: mime );

const fd = new FormData();
fd.append("avatar", imageCrop);

// send fd to axios post method. 
// You should pass in post request "Content-Type": "multipart/form-data" inside headers.

【讨论】:

感谢您澄清这一点。我已经更新了我的代码以使用正确的标头作为 multipart/form-data 提交。我还使用 axios 将 blob 从预览 url 转换为文件。我正在使用 react 钩子和 react-image-crop 而不是您引用的库。我已根据您的建议更新了我的帖子,并进行了更改。我仍然收到 400 错误。您能告诉我如何在 onSubmit() 函数中转换 blob 文件吗? 在 onSubmit() 中创建文件后,我在控制台记录了文件,并在 Submit() 中添加了所有项目后的 formData。我还从crop preview src 添加了blob url。我将这三个都添加到了帖子的末尾。当我用我的代码运行你的代码时,我得到 TypeError: file.split is not a function 或cropper.getCroppedCanvas() is not a function。可能是由于库不同。感谢您的所有帮助。 我提供的解决方案适用于 react-cropper (npmjs.com/package/react-cropper)。如果它对您的情况也有用,您可以使用它。 谢谢,我会研究一下这个库。

以上是关于如何使用 React.js + Django Rest Framework 保存带有表单提交的 blob 文件的主要内容,如果未能解决你的问题,请参考以下文章

如何在django api调用的react js中使用来自post请求的响应部分的数据?

React.js 使用 JQuery 发布 Django 得到 403

如何使用 CKEditor 和 React JS 捕获 OnChange 事件

如何使用 j_security_check 在 React.JS 应用程序上对用户进行身份验证

Facebook React.js :: 为客户端渲染传递数据

React.js框架新人手把手教你如何搭建项目