将图像文件从 React 前端上传到 Node/Express/Mongoose/MongoDB 后端(不工作)

Posted

技术标签:

【中文标题】将图像文件从 React 前端上传到 Node/Express/Mongoose/MongoDB 后端(不工作)【英文标题】:Upload image file from React front end to Node/Express/Mongoose/MongoDB back end (not working) 【发布时间】:2019-04-16 21:17:16 【问题描述】:

我花了一天的大部分时间来研究这个问题并试图让它发挥作用。这是一个带有 React/Redux 前端和 Node/Express/Mongoose/MongoDB 后端的应用程序。

我目前有一个主题系统,授权用户可以关注/取消关注主题,管理员可以添加/删除主题。 我希望在提交新主题时能够上传图像文件,并且我想使用 Cloudinary 来存储图像,然后将图像路径保存到带有主题名称的数据库中。

我遇到的问题是我无法从前端接收到后端上传的文件。尽管进行了大量的研究和试验/错误,我最终还是收到了一个空对象。我还没有完成 Cloudinary 文件上传的设置,但我需要先在后端接收文件,然后再担心。

服务器端 index.js:

const express = require("express");
const http = require("http");
const bodyParser = require("body-parser");
const morgan = require("morgan");
const app = express();
const router = require("./router");
const mongoose = require("mongoose");
const cors = require("cors");
const fileUpload = require("express-fileupload");
const config = require("./config");

const multer = require("multer");
const cloudinary = require("cloudinary");
const cloudinaryStorage = require("multer-storage-cloudinary");

app.use(fileUpload());

//file storage setup
cloudinary.config(
  cloud_name: "niksauce",
  api_key: config.cloudinaryAPIKey,
  api_secret: config.cloudinaryAPISecret
);

const storage = cloudinaryStorage(
  cloudinary: cloudinary,
  folder: "images",
  allowedFormats: ["jpg", "png"],
  transformation: [ width: 500, height: 500, crop: "limit" ] //optional, from a demo
);

const parser = multer( storage: storage );

//DB setup
mongoose.Promise = global.Promise;
mongoose.connect(
  `mongodb://path/to/mlab`,
   useNewUrlParser: true 
);

mongoose.connection
  .once("open", () => console.log("Connected to MongoLab instance."))
  .on("error", error => console.log("Error connecting to MongoLab:", error));

//App setup
app.use(morgan("combined"));
app.use(bodyParser.json( type: "*/*" ));
app.use(bodyParser.urlencoded( extended: true ));
app.use(cors());
router(app, parser);

//Server setup
const port = process.env.PORT || 3090;
const server = http.createServer(app);
server.listen(port);
console.log("server listening on port: ", port);

TopicController/CreateTopic

exports.createTopic = function(req, res, next) 
  console.log("REQUEST: ", req.body); // name: 'Topic with Image', image:  
  console.log("IMAGE FILE MAYBE? ", req.file); //undefined
  console.log("IMAGE FILES MAYBE? ", req.files); //undefined

  const topic = new Topic(req.body);
  if (req.file) 
    topic.image.url = req.file.url;
    topic.image.id = req.file.publid_id;
   else 
    console.log("NO FILE UPLOADED");
  

  topic.save().then(result => 
    res.status(201).send(topic);
  );
;

路由器.js

module.exports = function(app, parser) 
  //User
  app.post("/signin", requireSignin, Authentication.signin);
  app.post("/signup", Authentication.signup);
  //Topic
  app.get("/topics", Topic.fetchTopics);
  app.post("/topics/newTopic", parser.single("image"), Topic.createTopic);
  app.post("/topics/removeTopic", Topic.removeTopic);
  app.post("/topics/followTopic", Topic.followTopic);
  app.post("/topics/unfollowTopic", Topic.unfollowTopic);
;

客户端

Topics.js:

import React,  Component  from "react";
import  connect  from "react-redux";
import  Loader, Grid, Button, Icon, Form  from "semantic-ui-react";

import 
  fetchTopics,
  followTopic,
  unfollowTopic,
  createTopic,
  removeTopic
 from "../actions";

import requireAuth from "./hoc/requireAuth";

import Background1 from "../assets/images/summer.jpg";
import Background2 from "../assets/images/winter.jpg";

const compare = (arr1, arr2) => 
  let inBoth = [];
  arr1.forEach(e1 =>
    arr2.forEach(e2 => 
      if (e1 === e2) 
        inBoth.push(e1);
      
    )
  );
  return inBoth;
;

class Topics extends Component 
  constructor(props) 
    super(props);

    this.props.fetchTopics();
    this.state = 
      newTopic: "",
      selectedFile: null,
      error: ""
    ;
  

  onFollowClick = topicId => 
    const  id  = this.props.user;

    this.props.followTopic(id, topicId);
  ;

  onUnfollowClick = topicId => 
    const  id  = this.props.user;

    this.props.unfollowTopic(id, topicId);
  ;

  handleSelectedFile = e => 
    console.log(e.target.files[0]);
    this.setState(
      selectedFile: e.target.files[0]
    );
  ;

  createTopicSubmit = e => 
    e.preventDefault();
    const  newTopic, selectedFile  = this.state;
    this.props.createTopic(newTopic.trim(), selectedFile);

    this.setState(
      newTopic: "",
      selectedFile: null
    );
  ;

  removeTopicSubmit = topicId => 
    this.props.removeTopic(topicId);
  ;

  renderTopics = () => 
    const  topics, user  = this.props;

    const followedTopics =
      topics &&
      user &&
      compare(topics.map(topic => topic._id), user.followedTopics);

    console.log(topics);

    return topics.map((topic, i) => 
      return (
        <Grid.Column className="topic-container" key=topic._id>
          <div
            className="topic-image"
            style=
              background:
                i % 2 === 0 ? `url($Background1)` : `url($Background2)`,
              backgroundRepeat: "no-repeat",
              backgroundPosition: "center",
              backgroundSize: "cover"
            
          />
          <p className="topic-name">topic.name</p>
          <div className="topic-follow-btn">
            followedTopics.includes(topic._id) ? (
              <Button
                icon
                color="olive"
                onClick=() => this.onUnfollowClick(topic._id)
              >
                Unfollow
                <Icon color="red" name="heart" />
              </Button>
            ) : (
              <Button
                icon
                color="teal"
                onClick=() => this.onFollowClick(topic._id)
              >
                Follow
                <Icon color="red" name="heart outline" />
              </Button>
            )
            /* Should put a warning safety catch on initial click, as to not accidentally delete an important topic */
            user.isAdmin ? (
              <Button
                icon
                color="red"
                onClick=() => this.removeTopicSubmit(topic._id)
              >
                <Icon color="black" name="trash" />
              </Button>
            ) : null
          </div>
        </Grid.Column>
      );
    );
  ;

  render() 
    const  loading, user  = this.props;

    if (loading) 
      return (
        <Loader active inline="centered">
          Loading
        </Loader>
      );
    

    return (
      <div>
        <h1>Topics</h1>
        user && user.isAdmin ? (
          <div>
            <h3>Create a New Topic</h3>
            <Form
              onSubmit=this.createTopicSubmit
              encType="multipart/form-data"
            >
              <Form.Field>
                <input
                  value=this.state.newTopic
                  onChange=e => this.setState( newTopic: e.target.value )
                  placeholder="Create New Topic"
                />
              </Form.Field>
              <Form.Field>
                <label>Upload an Image</label>
                <input
                  type="file"
                  name="image"
                  onChange=this.handleSelectedFile
                />
              </Form.Field>
              <Button type="submit">Create Topic</Button>
            </Form>
          </div>
        ) : null

        <Grid centered>this.renderTopics()</Grid>
      </div>
    );
  


const mapStateToProps = state => 
  const  loading, topics  = state.topics;
  const  user  = state.auth;

  return  loading, topics, user ;
;

export default requireAuth(
  connect(
    mapStateToProps,
     fetchTopics, followTopic, unfollowTopic, createTopic, removeTopic 
  )(Topics)
);

TopicActions/createTopic:

export const createTopic = (topicName, imageFile) => 
  console.log("IMAGE IN ACTIONS: ", imageFile); //this is still here 
  // const data = new FormData();
  // data.append("image", imageFile);
  // data.append("name", topicName);

  const data = 
    image: imageFile,
    name: topicName
  ;
  console.log("DATA TO SEND: ", data); //still shows image file 
  return dispatch => 
    // const config =  headers:  "Content-Type": "multipart/form-data"  ;
        // ^ this fixes nothing, only makes the problem worse 

    axios.post(CREATE_NEW_TOPIC, data).then(res => 
      dispatch(
        type: CREATE_TOPIC,
        payload: res.data
      );
    );
  ;
;

当我这样发送时,我会在后端收到以下信息: (这些是服务器 console.logs) 请求:图片:,名称:“新主题” 图像文件可能吗?不明确的 图像文件可能吗?不明确的 没有文件上传

如果我使用 new FormData() 路由,FormData 是一个空对象,我会收到以下服务器错误: 发布http://localhost:3090/topics/newTopicnet::ERR_EMPTY_RESPONSE

export const createTopic = (topicName, imageFile) => 
  console.log("IMAGE IN ACTIONS: ", imageFile);
  const data = new FormData();

  data.append("image", imageFile);
  data.append("name", topicName);

  // const data = 
  //   image: imageFile,
  //   name: topicName
  // ;
  console.log("DATA TO SEND: ", data); // shows FormData  (empty object, nothing in it)
  return dispatch => 
    // const config =  headers:  "Content-Type": "multipart/form-data"  ;
    // ^ this fixes nothing, only makes the problem worse

    axios.post(CREATE_NEW_TOPIC, data).then(res => 
      dispatch(
        type: CREATE_TOPIC,
        payload: res.data
      );
    );
  ;
;

【问题讨论】:

打开你的开发工具,网络选项卡,看看多部分数据是否在请求的后半部分可用(在Request Headers之后)。请在此处粘贴该部分 接受:application/json, text/plain, / Accept-Encoding: gzip, deflate, br Accept-Language: en-US,en;q=0.9 Connection: keep-alive Content-Length: 37 Content-Type: application/json;charset=UTF-8 Host: localhost:3090 Origin: localhost:3000 Referer: localhost:3000/topics User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (Khtml, like Gecko) Chrome/70.0.3538.77 Safari/537.36 不,我指的是后面的字段。这些标头后面是什么? " ..this 解决不了任何问题,.." 那么你到底在哪里使用过config 变量呢?我在代码中看不到任何内容,甚至没有看到显示您使用它的注释尝试。见How do I set multipart in axios with react? 很好的链接,将尝试该模式。是的,我在 axios 帖子中有那个配置变量,只是删除而不是注释掉。 @Masious 只是请求载荷,如下: image: , name: "name I submit" 【参考方案1】:

解决方案是改用 Firebase,并在 React 客户端上处理图像上传(尝试使用 cloudinary 但没有成功)。生成的下载 url 可以与主题名称一起保存到数据库中(这是我想要的 cloudinary 的全部内容),现在它与主题一起显示正确的图像。

【讨论】:

以上是关于将图像文件从 React 前端上传到 Node/Express/Mongoose/MongoDB 后端(不工作)的主要内容,如果未能解决你的问题,请参考以下文章

从 Postgres 返回图像 url 到前端 - Node / React

如何使用 React 前端从 Mongoose 显示带有 <img> 的图像

使用react native将图像上传到heroku

如何将图片从前端上传到后端

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

如何将图像上传到 KeystoneJS GraphQL 端点?