簡體   English   中英

將圖像文件從React前端上傳到Node / Express / Mongoose / MongoDB后端(不起作用)

[英]Upload image file from React front end to Node/Express/Mongoose/MongoDB back end (not working)

我花了整整一天的時間研究此問題,並努力使其正常工作。 這是一個具有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)請求:{image:{},名稱:'NEW TOPIC'}可能是圖像文件? 可能未定義的圖像文件? 未定義未上傳文件

如果我使用新的FormData()路由,則FormData是一個空對象,並且出現此服務器錯誤:POST http:// localhost:3090 / topics / newTopic net :: 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
      });
    });
  };
};

解決方案是改用Firebase,並在React客戶端上處理圖像上傳(嘗試使用cloudinary,但沒有成功)。 可以將生成的下載URL與主題名稱(這是我從cloudinary想要的)一起保存到數據庫中,現在它將顯示正確的圖像以及主題。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM