MERN/Redux 应用程序:图片上传功能正在开发中,而非生产中

MERN/Redux App: Image upload feature works in development, not production

React/Node/Express 带有 Redux 的应用,连接到 Mongo Atlas DB Cloud

大家好。我为想要上传和展示他的艺术作品的艺术家创建了一个带有完整 CRUD 的基本作品集应用程序(在我的代码库中称为“佣金”)。

我按照这篇文章了解如何将图片上传到 mongoose:

https://codeburst.io/image-uploading-using-react-and-node-to-get-the-images-up-c46ec11a7129

基本上,本文使用的策略允许您在上传图片时做两件事:

1 - saves/sends mongo 将文档发送到 mongoDB(在我的例子中,Mongo Atlas Cloud)

2 - 将图像保存在应用程序的本地文件目录中

它在本地有效。我可以上传任意数量的 files/images,它全部显示在组件上,发送到 mongo 并保存在 app/uploads/

然而,一旦我将应用程序部署到 Heroku,它就不允许我上传任何图像。并且本地上传的图片不显示,只显示其他内容,如commission.titlecommission.description

代码

佣金模式

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const CommissionSchema = new Schema({
  imageName: {
    type: String,
    default: "none",
    required: true
  },
  imageData: {
    type: String,
    required: true
  },
  title: {
    type: String,
    maxlength: 22,
    required: true
  },
  description: {
    type: String,
    required: true
  },
  price: {
    type: Number,
    required: false
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

module.exports = Commission = mongoose.model('commission', CommissionSchema);

通过 Multer 上传中间件

const multer = require('multer');

const storage = multer.diskStorage({
  destination: function(req, file, cb) {
    cb(null, './uploads/');
  },
  filename: function(req, file, cb) {
    cb(null, Date.now() + file.originalname);
  }
});

const fileFilter = (req, file, cb) => {
  if(file.mimetype === 'image/jpeg' || file.mimetype === 'image/jpg' || file.mimetype === 'image/png') {
    cb(null, true);
  } else {
    cb(null, false);
  };
};

const upload = multer({
  storage: storage,
  limits: {
    fileSize: 1024 * 1024 * 5
  },
  fileFilter: fileFilter
});

module.exports = upload;

Post 新佣金路线

const express = require('express');
const router = express.Router();
const upload = require('../../middleware/upload');
const Commission = require('../../models/Commission');

router.route('/').post(upload.single('imageData'), (req, res) => {
  const newCommission = new Commission({
    imageName: req.body.imageName,
    imageData: req.file.path,
    title: req.body.title,
    description: req.body.description,
    price: req.body.price
  });
  newCommission.save()
    .then(commission => res.json(commission))
    .catch(err => res.status(400).json(`Create new commission failed: ${err}`));
});

axios 的 Redux 操作 post

export const addCommission = commission => (dispatch, getState) => {
  axios
    .post('/commissions', commission, tokenConfig(getState))
    .then(res => dispatch({
      type: ADD_COMMISSION,
      payload: res.data
    }))
    .catch(err => {
      dispatch(returnErrors(err.response.data, err.response.status));
      dispatch({ type: ADD_COMMISSION_FAIL });
    });  
  axios
    .get('/commissions')
    .then(res => dispatch({
      type: GET_COMMISSIONS,
      payload: res.data
    }));
};

表单组件(相关代码用<~~箭头表示)

import React, { Component } from 'react';
import {
  Col,
  Form,
  FormGroup,
  Label,
  Input,
  Button
} from 'reactstrap';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { addCommission } from '../../actions/commissionActions';     <~~<~~<~~     <~~<~~<~~
import { Redirect } from 'react-router-dom';
import ImagePreview from '../../images/ImagePreview.png';

class NewCommissionForm extends Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.handleImageChange = this.handleImageChange.bind(this);     <~~<~~<~~     <~~<~~<~~
    this.handleSubmit = this.handleSubmit.bind(this);
    this.state = {
      title: "",
      description: "",
      price: "",
      image: ImagePreview,
      redirectToCommissions: false
    };
  };

  componentDidMount() {
    if(this.state.redirectToCommissions) {
      this.setState({
        redirectToCommissions: false
      });
    };
  };

  static propTypes = {
    addCommission: PropTypes.func.isRequired,
    user: PropTypes.object.isRequired
  };

  handleChange(e) {
    this.setState({
      [e.target.name]: e.target.value
    });
  };

  handleImageChange(e) {
    this.setState({
      image: URL.createObjectURL(e.target.files[0])     <~~<~~<~~     <~~<~~<~~
    });
  };

  handleSubmit(e) {
    e.preventDefault();

    let imageFormObj = {};                                                  <~~<~~<~~     <~~<~~<~~
    imageFormObj = new FormData();                                          <~~<~~<~~     <~~<~~<~~
    imageFormObj.append("imageName", "multer-image-" + Date.now());         <~~<~~<~~     <~~<~~<~~
    imageFormObj.append("imageData", e.target.elements.image.files[0]);     <~~<~~<~~     <~~<~~<~~
    imageFormObj.append("title", this.state.title);                         <~~<~~<~~     <~~<~~<~~
    imageFormObj.append("description", this.state.description);             <~~<~~<~~     <~~<~~<~~
    imageFormObj.append("price", this.state.price);                         <~~<~~<~~     <~~<~~<~~
    this.props.addCommission(imageFormObj);                                 <~~<~~<~~     <~~<~~<~~

    this.setState({
      title: "",
      description: "",
      price: "",
      image: ImagePreview,
      redirectToCommissions: true
    })
  };

  render() {
    const redirectToCommissions = this.state.redirectToCommissions;
    const { isAuthenticated } = this.props.user;
    
    if(!isAuthenticated) {
      return (
        <h1 style={styles.accessDenied}>
          You don't have access to this page
        </h1>
      );
    } else {
      return (
        <div>
          <Form style={styles.container} autoFocus={false} onSubmit={this.handleSubmit}>
            <h1 style={styles.title}>Upload a new Commission</h1>
            
            <FormGroup row>
              <Label for="title" style={styles.labelText} sm={2}>Title</Label>
              <Col sm={10}>
                <Input
                  type="text"
                  name="title"
                  id="title"
                  maxLength="22"
                  autoFocus
                  required
                  onChange={this.handleChange}
                  value={this.state.title}
                />
              </Col>
            </FormGroup>
  
            <FormGroup row>
              <Label for="description" style={styles.labelText} sm={2}>Description</Label>
              <Col sm={10}>
                <Input
                  type="textarea"
                  name="description"
                  id="description"
                  required
                  onChange={this.handleChange}
                  value={this.state.description}
                />
              </Col>
            </FormGroup>
  
            <FormGroup row>
              <Label for="price" style={styles.labelText} sm={2}>Price</Label>
              <Col sm={10}>
                <Input
                  type="number"
                  name="price"
                  id="price"
                  min={0}
                  onChange={this.handleChange}
                  value={this.state.price}
                />
              </Col>
            </FormGroup>

            <FormGroup row>
              <Label for="image" style={styles.labelText} sm={2}>Image</Label>
              <Col sm={10}>
                <Input
                  type="file"
                  name="image"
                  id="image"
                  required
                  onChange={e => this.handleImageChange(e)}     <~~<~~<~~     <~~<~~<~~
                />
                <img
                  src={this.state.image}
                  alt="Commission Preview"
                  style={styles.imagePreview}
                />
              </Col>
            </FormGroup>
  
            <FormGroup row>
              <Col sm={2}>
              </Col>
              <Col sm={10}>
                <Button outline block color="info" style={styles.submitButton}>Submit</Button>
              </Col>
            </FormGroup>
          </Form>
          { redirectToCommissions ? <Redirect to="/" /> : null }
        </div>
      );
    }
  };
};

const styles = {
  container: {
    paddingLeft: '5%',
    paddingRight: '5%'
  },
  title: {
    paddingBottom: 50,
    textAlign: 'center'
  },
  labelText: {
    fontWeight: 'bold',
    textShadow: '2px 2px 4px black'
  },
  submitContainer: {
    paddingTop: 50,
    paddingLeft: 'auto',
    paddingRight: 'auto',
    display: 'flex',
    justifyContent: 'center'
  },
  submitButton: {
    fontSize: '1.2em',
    backgroundColor: 'black',
    color: 'white'
  },
  accessDenied: {
    textAlign:'center',
    paddingTop:50,
    paddingBottom:50
  },
  imagePreview: {
    marginTop: 15,
    width: 300,
    height: 300
  }
};

const mapStateToProps = state => ({
  commissions: state.commission.commissions,
  loading: state.commission.loading,
  user: state.user
});

export default connect(mapStateToProps, {addCommission})(NewCommissionForm);

我通过跟踪狂找到文章作者并联系他找到了解决方案。

  • 在本地上传临时图像(使用图像文件更新 root/uploads/ 目录)

  • 保存并commit/push到Git

  • 部署到 Heroku

仅此而已。当我部署应用程序时,我清理了 /uploads/ 文件夹(因为我不希望我在本地玩弄的测试图像出现在实际站点上)。

由于某种原因,这导致该功能无法在生产中使用。也许 Heroku 会在目录为空时忽略或清除该目录。

但是,使用目录中的文件部署它可以使该功能按预期工作。