将图像文件从 React 前端上传到 Node/Express/Mongoose/MongoDB 后端(不工作)
Posted
技术标签:
【中文标题】将图像文件从 React 前端上传到 Node/Express/Mongoose/MongoDB 后端(不工作)【英文标题】:Upload image file from React front end to Node/Express/Mongoose/MongoDB back end (not working) 【发布时间】:2019-04-16 21:17:16 【问题描述】:我花了一天的大部分时间来研究这个问题并试图让它发挥作用。这是一个带有 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.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)
);
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 FormData() 路由,FormData 是一个空对象,我会收到以下服务器错误: 发布http://localhost:3090/topics/newTopicnet::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
);
);
;
;
【问题讨论】:
打开你的开发工具,网络选项卡,看看多部分数据是否在请求的后半部分可用(在Request Headers
之后)。请在此处粘贴该部分
接受: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: localhost:3000 Referer: localhost:3000/topics 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
不,我指的是后面的字段。这些标头后面是什么?
" ..this 解决不了任何问题,.." 那么你到底在哪里使用过config
变量呢?我在代码中看不到任何内容,甚至没有看到显示您使用它的注释尝试。见How do I set multipart in axios with react?
很好的链接,将尝试该模式。是的,我在 axios 帖子中有那个配置变量,只是删除而不是注释掉。 @Masious 只是请求载荷,如下: image: , name: "name I submit"
【参考方案1】:
解决方案是改用 Firebase,并在 React 客户端上处理图像上传(尝试使用 cloudinary 但没有成功)。生成的下载 url 可以与主题名称一起保存到数据库中(这是我想要的 cloudinary 的全部内容),现在它与主题一起显示正确的图像。
【讨论】:
以上是关于将图像文件从 React 前端上传到 Node/Express/Mongoose/MongoDB 后端(不工作)的主要内容,如果未能解决你的问题,请参考以下文章
从 Postgres 返回图像 url 到前端 - Node / React
如何使用 React 前端从 Mongoose 显示带有 <img> 的图像