如何将 React Native 应用程序上的本地图像文件上传到 Rails api?

Posted

技术标签:

【中文标题】如何将 React Native 应用程序上的本地图像文件上传到 Rails api?【英文标题】:How to upload local image file on React Native app to Rails api? 【发布时间】:2019-02-19 10:25:44 【问题描述】:

我很难理解来自智能手机的本地文件路径是如何通过 Rails api 上传到服务器端的。

我们发送到后端的文件路径对服务器没有任何意义?

我从这样的响应中得到一个 uri:

file:///Users/.../Documents/images/5249F841-388B-478D-A0CB-2E1BF5511DA5.jpg):

我尝试向服务器发送这样的内容:

  let apiUrl = 'https://vnjldf.ngrok.io/api/update_photo'

  let uriParts = uri.split('.');
  let fileType = uri[uri.length - 1];

  let formData = new FormData();
  formData.append('photo', 
    uri,
    name: `photo.$fileType`,
    type: `image/$fileType`,
  );

  let options = 
    method: 'POST',
    body: formData,
    headers: 
      Accept: 'application/json',
      'Content-Type': 'multipart/form-data',
    ,
  ;

但我不确定它是什么以及如何在后端对其进行描述。

我也尝试过直接发送 uri,但我当然会收到以下错误:

Errno::ENOENT (No such file or directory @ rb_sysopen -...

任何帮助/指导将不胜感激。

【问题讨论】:

您在哪里看到此错误?在后端? 我看到您正在使用 Cloudinary。我建议将图像直接从客户端上传给他们,然后将 cloudinary_id 发布到服务器。 支持@BenToogood 请您提供您的控制器代码和错误的堆栈跟踪。 【参考方案1】:

我最近花了 1 个多小时调试类似的东西。

我发现如果你使用这个 json 从你的 React Native 应用程序向你的 Rails 后端进行 POST:

let formData = new FormData();
  formData.append('photo', 
    uri,
    name: `photo.$fileName`,
    type: `image/$fileType`,
  );

Rails 会自动在您的 params[:photo] 中为您提供一个 ActionDispatch::Http::UploadedFile,您可以像 Photo.create(photo: params[:photo]) 一样将其直接附加到您的模型上,而且它很简单。

但是,如果你不传递文件名,一切都会中断,你会得到一个巨大的字符串,它会引发ArgumentError (invalid byte sequence in UTF-8)

因此,根据您的代码,我可以发现错误:您将name 传递为photo.$fileType,这是错误的,应该是photo.$fileName(相应地更新以获取您的图像文件名...您的 React Native 代码中的 console.log(photo) 会显示正确的。

【讨论】:

你救了我的命。我的文件名是null,我在控制器中收到了一个超长字符串。通过设置一些文件名,我解决了这个问题!非常感谢 我花了几个小时才找到解决方案,谢谢您的回答【参考方案2】:

维护删除和添加新文件的问题

这就是我设法添加多个文件上传并维护删除和添加新文件的问题

class User < ApplicationRecord
  attribute :photos_urls # define it as an attribute so that seriallizer grabs it to generate JSON i.e. as_json method

  has_many_attached :photos


  def photos_urls
    photos.map do |ip|
      url: Rails.application.routes.url_helpers.url_for(ip), signed_id: ip.signed_id
    end
  end

See about signed_id here。它描述了如何处理多个文件上传。

控制器看起来像


  def update
    user = User.find(params[:id])
    if user.update(user_params)
      render json: 
        user: user.as_json(except: [:otp, :otp_expiry])
      , status: :ok
    else
      render json:  error: user.errors.full_messages.join(',') , status: :bad_request
    end
  end

...

  private

  def user_params
    params.permit(
      :id, :name, :email, :username, :country, :address, :dob, :gender,
      photos: []
    )
  end


React Native 部分

我正在使用react-native-image-crop-picker

  
import ImagePicker from 'react-native-image-crop-picker';

...

  const photoHandler = index => 
    ImagePicker.openPicker(
      width: 300,
      height: 400,
      multiple: true,
    ).then(selImages => 
      if (selImages && selImages.length == 1) 
        // Make sure, changes apply to that image-placeholder only which receives 'onPress' event
        // Using 'index' to determine that
        let output = images.slice();
        output[index] = 
          url: selImages[0].path, // For <Image> component's 'source' field
          uri: selImages[0].path, // for FormData to upload
          type: selImages[0].mime,
          name: selImages[0].filename,
        ;
        setImages(output);
       else 
        setImages(
          selImages.map(image => (
            url: image.path, // For <Image> component's 'source' field
            uri: image.path, // for FormData to upload
            type: image.mime,
            name: image.filename,
          )),
        );
      
    );
  ;

...

       <View style=style.imageGroup>
        images.map((item, index) => (
          <TouchableOpacity
            key=`img-$index`
            style=style.imageWrapper
            onPress=() => photoHandler(index)>
            <Image style=style.tileImage source=item />
          </TouchableOpacity>
        ))
      </View>

上传者的样子

// ../models/api/index.js

// Update User
export const updateUser = async ( id, data ) => 
  // See https://developer.mozilla.org/en-US/docs/Web/API/FormData/append
  let formData = new FormData(data);

  for (let key in data) 
    if (Array.isArray(data[key])) 
      // If it happens to be an Image field with multiple support
      for (let image in data[key]) 
        if (data[key][image]?.signed_id) 
          // if the data has not change and it is as it was downloaded from server then
          // it means you do not need to delete it
          // For perverving it in DB you need to send `signed_id`
          formData.append(`$key[]`, data[key][image].signed_id);
         else if (data[key][image]?.uri && data[key][image]?.url) 
          // if the data has change and it is as it has been replaced because user selected a different image in place
          // it means you need to delete it and replace it with new one
          // For deleting it in DB you should not send `signed_id`
          formData.append(`$key[]`, data[key][image]);
        
      
     else 
      formData.append(key, data[key]);
    
  
  return axios.patch(BASE_URL + "/users/" + data.id, formData, 
    headers: 
      'Content-Type': 'multipart/form-data',
    ,
  );
;

Saga 工人看起来像

import * as Api from "../models/api";

// worker Saga:
function* updateUserSaga( payload ) 
  console.log('updateUserSaga: payload', payload);
  try 
    const response = yield call(Api.updateUser, 
      id: payload.id,
      data: payload,
    );
    if (response.status == 200) 
      yield put(userActions.updateUserSuccess(response.data));
      RootNavigation.navigate('HomeScreen');
     else 
      yield put(userActions.updateUserFailure( error: response.data.error ));
    
   catch (e) 
    console.error('Error: ', e);
    yield put(
      userActions.updateUserFailure(
        error: "Network Error: Could not send OTP, Please try again.",
      )
    );
  

【讨论】:

以上是关于如何将 React Native 应用程序上的本地图像文件上传到 Rails api?的主要内容,如果未能解决你的问题,请参考以下文章

如何创建 POST 请求以从本地路径将文件存储在服务器上 - React Native

Android上的react-native-webview无法使用ERR_ACCESS_DENIED加载保存在缓存目录中的本地文件[重复]

如何将库的本地副本添加和编译到 Android React Native 模块中?

获取不在React Native中发送正文

将本地 web 应用程序包含到 react native webview 中

如何使用 React-native 访问 Django 本地服务器数据?