Загрузите файл изображения из интерфейса React в серверную часть Node / Express / Mongoose / MongoDB (не работает)

Я провел большую часть дня, изучая это и пытаясь заставить его работать. Это приложение с интерфейсом 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.info("Connected to MongoLab instance."))
  .on("error", error => console.info("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.info("server listening on port: ", port);

TopicController / CreateTopic

exports.createTopic = function(req, res, next) {
  console.info("REQUEST: ", req.body); //{ name: 'Topic with Image', image: {} }
  console.info("IMAGE FILE MAYBE? ", req.file); //undefined
  console.info("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.info("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.info(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.info(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.info("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.info("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.infos) ЗАПРОС: {изображение: {}, имя: 'НОВАЯ ТЕМА'} ФАЙЛ ИЗОБРАЖЕНИЯ МОЖЕТ БЫТЬ? неопределенный ФАЙЛЫ ИЗОБРАЖЕНИЙ МОГУТ БЫТЬ? неопределенный ФАЙЛ НЕ ЗАГРУЖЕН

Если я пойду по новому маршруту FormData (), FormData будет пустым объектом, и я получу эту ошибку сервера: POST http: // локальный: 3090 / themes / newTopic net :: ERR_EMPTY_RESPONSE

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

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

  // const data = {
  //   image: imageFile,
  //   name: topicName
  // };
  console.info("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). Пожалуйста, вставьте эту часть сюда

Masious 14.11.2018 02:31

Принять: 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: локальный: 3000 Referer: localhost: 3000 / тем 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

Nik Hammer-Ellis 14.11.2018 02:56

Нет, я имел в виду поле после этого. Что стоит после этих заголовков?

Masious 14.11.2018 02:57
"..это ничего не исправляет, .." Так где именно вы когда-либо использовали переменную config? Я не вижу ничего в коде или даже комментированной попытки, показывающей, что вы его используете. См. Как установить multipart в аксиомах с помощью React?
Neil Lunn 14.11.2018 03:01

Хорошая ссылка, попробую этот шаблон. Да, у меня была эта переменная конфигурации в сообщении axios, просто удалена, а не закомментирована. @Masious просто полезная нагрузка запроса, а именно: {image: {}, name: "имя, которое я отправил"}

Nik Hammer-Ellis 14.11.2018 03:06

Как видите, ваше изображение не загружено на сервер, поскольку ключ image является пустым объектом JSON. Теперь попробуйте еще раз и вставьте данные с переменной config без комментариев и используемые в Axios.post.

Masious 14.11.2018 03:08

Есть идеи по разрешению этого? Доступ к XMLHttpRequest по адресу «локальный: 3090 / темы / новая тема» из источника «локальный: 3000» был заблокирован политикой CORS: на запрошенном ресурсе отсутствует заголовок «Access-Control-Allow-Origin».

Nik Hammer-Ellis 14.11.2018 03:11

Заголовок вызывает эту ошибку

Nik Hammer-Ellis 14.11.2018 03:13

Вам необходимо включить CORS на сервере. Просто добавьте это: app.use(cors()) в ваш серверный index.js после запуска npm install cors в корне сервера.

Masious 14.11.2018 03:13

Я уже реализовал это, перепроверьте индекс моего сервера выше.

Nik Hammer-Ellis 14.11.2018 03:14

В частности, я получаю это сейчас Ошибка: Multipart: граница не найдена

Nik Hammer-Ellis 14.11.2018 04:12
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
11
1 646
1

Ответы 1

Решение заключалось в том, чтобы вместо этого переключиться на использование Firebase и заняться загрузкой изображений на клиенте React (эта попытка была предпринята с облачным хранилищем, но безуспешно). Полученный URL-адрес загрузки можно сохранить в базе данных с именем темы (это все, что я хотел от облачного хранилища), и теперь он отображает правильные изображения вместе с темами.

Другие вопросы по теме