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

Node.js 将图像文件从React前端上传到Node/Express/Mongoose/MongoDB后端(不工作),node.js,reactjs,express,file-upload,cloudinary,Node.js,Reactjs,Express,File Upload,Cloudinary,我花了一天的时间来研究这个问题,并试图让它发挥作用。这是一个具有React/Redux前端和Node/Express/Mongoose/MongoDB后端的应用程序 我目前有一个主题系统,授权用户可以关注/取消关注主题,管理员可以添加/删除主题。 我希望能够在提交新主题时上载图像文件,并且我希望使用Cloudinary存储图像,然后使用主题名称将图像路径保存到DB 我遇到的问题是,我无法从前端接收后端上传的文件。尽管进行了大量的研究和尝试/错误,我最终还是收到了一个空的对象。我还没有完成Clou

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

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

我遇到的问题是,我无法从前端接收后端上传的文件。尽管进行了大量的研究和尝试/错误,我最终还是收到了一个空的对象。我还没有完成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)
);
当我这样发送时,我会在后端收到以下内容: (这些是服务器控制台日志) 请求:{image:{},名称:'newtopic'} 也许是图像文件?未定义 也许是图像文件?未定义 没有上载文件

如果我选择new FormData()路径,FormData是一个空对象,我会得到以下服务器错误: POST net::ERR\u EMPTY\u响应

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

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

打开devtools,network选项卡,查看多部分数据是否在请求的后半部分可用(在
请求头之后)。请将该部分粘贴到此处接受:application/json,text/plain,/Accept编码:gzip,deflate,br接受语言:en-US,en;q=0.9连接:保持活动内容长度:37内容类型:应用程序/json;charset=UTF-8主机:localhost:3090来源:Referer:User-Agent:Mozilla/5.0(麦金塔;英特尔Mac OS X 10_14_1)AppleWebKit/537.36(KHTML,像Gecko)Chrome/70.0.3538.77 Safari/537.36不,我指的是这个字段后面的字段。这些标题后面是什么?“…这没有修复任何问题,…”那么您究竟在哪里使用过
config
变量?我在代码中看不到任何东西,甚至看不到显示您正在使用它的注释尝试。看到好的链接,我会尝试这种模式。是的,我在axios帖子中有一个配置变量,只是被删除了,而不是注释掉了@Masious只是请求负载,如下所示:{image:{},name:“name I submitted”}
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
      });
    });
  };
};