Я провел большую часть дня, изучая это и пытаясь заставить его работать. Это приложение с интерфейсом 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
});
});
};
};
Принять: 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
Нет, я имел в виду поле после этого. Что стоит после этих заголовков?
config? Я не вижу ничего в коде или даже комментированной попытки, показывающей, что вы его используете. См. Как установить multipart в аксиомах с помощью React?Хорошая ссылка, попробую этот шаблон. Да, у меня была эта переменная конфигурации в сообщении axios, просто удалена, а не закомментирована. @Masious просто полезная нагрузка запроса, а именно: {image: {}, name: "имя, которое я отправил"}
Как видите, ваше изображение не загружено на сервер, поскольку ключ image является пустым объектом JSON. Теперь попробуйте еще раз и вставьте данные с переменной config без комментариев и используемые в Axios.post.
Есть идеи по разрешению этого? Доступ к XMLHttpRequest по адресу «локальный: 3090 / темы / новая тема» из источника «локальный: 3000» был заблокирован политикой CORS: на запрошенном ресурсе отсутствует заголовок «Access-Control-Allow-Origin».
Заголовок вызывает эту ошибку
Вам необходимо включить CORS на сервере. Просто добавьте это: app.use(cors()) в ваш серверный index.js после запуска npm install cors в корне сервера.
Я уже реализовал это, перепроверьте индекс моего сервера выше.
В частности, я получаю это сейчас Ошибка: Multipart: граница не найдена





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