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

时间:2018-11-13 23:53:51

标签: node.js reactjs express file-upload cloudinary

我花了整整一天的时间研究这个问题,并努力使它生效。这是一个具有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);
  });
};

router.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 TOPIC'} 图像文件可能吗?未定义 图像文件可能吗?未定义 没有文件上传

如果我使用新的FormData()路由,则FormData是一个空对象,并且出现此服务器错误: POST http://localhost:3090/topics/newTopic网:: 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
      });
    });
  };
};

1 个答案:

答案 0 :(得分:0)

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