如何在github上托管的(Netlify)中将文件从后端(Heroku)上传到前端



我开发了一个上传到Github的应用程序,我使用Heroku从Github托管(后端文件夹(,使用(自动部署(,还使用Netlify托管(前端文件夹(,它在我的本地计算机上运行得很好,但当我尝试从前端的表单上传文件时,它会向后端发送一个请求,后端会自行将文件保存到位于前端目录中的/uploads文件夹中。

我的文件结构如下:

[Server]
- controllers
- - food.js
[Client]
- public
-- uploads
- src
-- pages
--- dashboard
---- food
----- AddFood.js

它在localhost上运行得很好,下面是我的代码:(客户端(AddFood.js:

import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import Axios from 'axios'
import useDocumentTitle from '../../../hooks/useDocumentTitle'
import Modal from '../../../components/Modal/Modal'
import { Success, Error, Loading } from '../../../components/Icons/Status'
import { createSlug } from '../../../functions/slug'
import goTo from '../../../functions/goTo'
const AddFood = () => {
useDocumentTitle('Add Food')
//Form States
const [foodName, setFoodName] = useState('')
const [foodPrice, setFoodPrice] = useState('')
const [foodDesc, setFoodDesc] = useState('')
const [foodFile, setFoodFile] = useState('')
const [preview, setPreview] = useState()
const [addFoodStatus, setAddFoodStatus] = useState()
const [addFoodMessage, setAddFoodMessage] = useState()
//Form errors messages
const ImgErr = document.querySelector('[data-form-img-msg]')
const foodNameErr = document.querySelector('[data-form-name-msg]')
const priceErr = document.querySelector('[data-form-price-msg]')
const descErr = document.querySelector('[data-form-desc-msg]')
const formMsg = document.querySelector('[data-form-msg]')
const modalLoading = document.querySelector('#modal')
const BASE_URL =
process.env.NODE_ENV === 'development'
? process.env.REACT_APP_API_LOCAL_URL
: process.env.REACT_APP_API_URL
const updateFoodImg = e => {
const file = e.target.files[0]
if (file) {
const fileType = file.type.split('/')[0]
if (fileType === 'image') setFoodFile(file)
const fileSizeToMB = file.size / 1000000
const MAX_FILE_SIZE = 1 //mb
if (fileSizeToMB > MAX_FILE_SIZE) {
if (ImgErr)
ImgErr.textContent = `file size can't be more than ${MAX_FILE_SIZE} MB`
} else {
ImgErr.textContent = ''
}
}
}
useEffect(() => {
// if there's an image
if (foodFile) {
const reader = new FileReader()
reader.onloadend = () => setPreview(reader.result)
reader.readAsDataURL(foodFile)
} else {
setPreview(null)
}
}, [foodFile])
const handleAddFood = async e => {
e.preventDefault()
//using FormData to send constructed data
const formData = new FormData()
formData.append('foodName', foodName)
formData.append('foodPrice', foodPrice)
formData.append('foodDesc', foodDesc)
formData.append('foodImg', foodFile)
if (
ImgErr.textContent === '' &&
foodNameErr.textContent === '' &&
priceErr.textContent === '' &&
descErr.textContent === ''
) {
//show waiting modal
modalLoading.classList.remove('hidden')
try {
const response = await Axios.post(`${BASE_URL}/foods`, formData)
const { foodAdded, message } = response.data
setAddFoodStatus(foodAdded)
setAddFoodMessage(message)
//Remove waiting modal
setTimeout(() => {
modalLoading.classList.add('hidden')
}, 300)
} catch (err) {
formMsg.textContent = `Sorry something went wrong ${err}`
}
} else {
formMsg.textContent = 'please add all details'
}
}
return (
<>
{addFoodStatus === 1 ? (
<Modal
status={Success}
msg='Added food'
redirectLink='menu'
redirectTime='3000'
/>
) : addFoodStatus === 0 ? (
<Modal
status={Error}
msg={addFoodMessage}
msg=''
/>
) : null}
<section className='py-12 my-8 dashboard'>
<div className='container mx-auto'>
<h3 className='mx-0 mt-4 mb-12 text-2xl text-center'>Add food</h3>
<div>
<div className='food'>
{/* Show Modal Loading when submitting form */}
<Modal
status={Loading}
modalHidden='hidden'
classes='text-blue-500 text-center'
msg='Please wait'
/>
<form
method='POST'
className='form'
encType='multipart/form-data'
onSubmit={handleAddFood}
>
<label className='flex flex-wrap items-center justify-center gap-4 mb-8 sm:justify-between'>
<img
src={
preview === null
? 'https://source.unsplash.com/random?food'
: preview
}
alt='food' //change with food image name
className='object-cover p-1 border border-gray-400 w-28 h-28 dark:border-gray-300 rounded-xl'
/>
<input
type='file'
name='foodImg'
id='foodImg'
accept='image/*'
onChange={updateFoodImg}
className='grow-[.7] cursor-pointer text-lg text-white p-3 rounded-xl bg-orange-800 hover:bg-orange-700 transition-colors'
required
/>
<span
className='inline-block my-2 text-red-400 font-[600]'
data-form-img-msg
></span>
</label>
<label htmlFor='foodName' className='form-group'>
<input
type='text'
id='foodName'
className='form-input'
autoFocus
required
onChange={e => setFoodName(createSlug(e.target.value.trim()))}
/>
<span className='form-label'>Food Name</span>
<span
className='inline-block my-2 text-red-400 font-[600]'
data-form-name-msg
></span>
</label>
<label htmlFor='foodPrice' className='form-group'>
<input
type='number'
id='foodPrice'
className='form-input'
min='5'
max='500'
required
onChange={e => setFoodPrice(e.target.value.trim())}
/>
<span className='form-label'>Price</span>
<span
className='inline-block my-2 text-red-400 font-[600]'
data-form-price-msg
></span>
</label>
<label htmlFor='foodDescription' className='form-group'>
<textarea
name='foodDescription'
id='foodDescription'
minLength='10'
maxLength='300'
className='form-input'
required
onChange={e => setFoodDesc(e.target.value.trim())}
></textarea>
<span className='form-label'>Description</span>
<span
className='inline-block my-2 text-red-400 font-[600]'
data-form-desc-msg
></span>
</label>
<div
className='my-14 text-red-400 font-[600] text-center text-xl'
data-form-msg
></div>
<div className='flex items-center justify-evenly'>
<button
type='submit'
className='min-w-[7rem] bg-green-600 hover:bg-green-700 text-white py-1.5 px-6 rounded-md'
>
Add
</button>
<Link
to={goTo('menu')}
className='text-gray-800 underline-hover text-bold dark:text-white'
>
Food Menu
</Link>
</div>
</form>
</div>
</div>
</div>
</section>
</>
)
}
export default AddFood

(服务器(foods.js:

const FoodsModel = require(`${__dirname}/../models/food-model.js`)
const { v4: uuidv4 } = require('uuid')
const sharp = require('sharp')
const deleteFile = require('../functions/deleteFile')
const addFood = async (req, res) => {
const { foodName, foodPrice, foodDesc } = req.body
const { foodImg } = req.files
const foodImgName = uuidv4() + foodImg.name
const foodImgMovePath = `${__dirname}/../../client/public/uploads/${foodImgName}.webp`
const foodImgDisplayPath = `/uploads/${foodImgName}`
const foods = new FoodsModel({
foodImgDisplayPath,
foodName,
foodPrice,
foodDesc
})
sharp(foodImg.data)
.rotate()
.resize(200)
.jpeg({ mozjpeg: true, quality: 50 })
.toBuffer()
.then(newBuffer => {
//changing the old jpg image buffer to new webp buffer
foodImg.data = newBuffer
foodImg.mv(foodImgMovePath, err => {
if (err) {
res.json({
message: `Sorry something wrong with server! 😥: ${err}`
})
return
}
foods.save()
res.json({
message: 'Food Added Successfully',
foodAdded: 1
})
})
})
.catch(err => {
res.json({
//https://mhmdhidr-restaurant.netlify.app/uploads/20cc09a0-1811-48b0-bffa-49e7a1981537chicken-legs.webp
message: `Sorry! Something went wrong, check the error => 😥: n ${err}`,
foodAdded: 0
})
})
}
const getFood = async (req, res) => {
res.json(res.paginatedResults)
}
const deleteFood = async (req, res) => {
const { prevFoodImg } = req.body
const { foodId } = req.params
deleteFile(prevFoodImg)
try {
await FoodsModel.findByIdAndRemove(foodId)
res.json({
message: 'Food Deleted Successfully',
foodDeleted: 1
})
} catch (error) {
res.json({
message: `Sorry! Something went wrong, check the error => 😥: n ${error}`,
foodDeleted: 0
})
}
}
const updateFood = async (req, res) => {
const { foodName, foodPrice, foodDesc, prevFoodImg } = req.body
const { foodId } = req.params
const { foodImg } = req.files || ''
const foodImgName = uuidv4() + foodImg?.name || ''
const foodImgMovePath = `${__dirname}/../../client/public/uploads/${foodImgName || ''}`
const foodImgDisplayPath =
foodImg !== '' && foodImg !== undefined ? `/uploads/${foodImgName}` : prevFoodImg
try {
await FoodsModel.findByIdAndUpdate(foodId, {
foodImgDisplayPath,
foodName,
foodPrice,
foodDesc
})
if (foodImg !== '' && foodImg !== undefined) {
deleteFile(prevFoodImg)
foodImg.mv(foodImgMovePath, err => {
if (err) {
res.json({ message: `Sorry something wrong with server! 😥: ${err}` })
}
})
}
res.json({
message: 'Food Updated Successfully',
foodUpdated: 1
})
} catch (error) {
res.json({
message: `Sorry! Something went wrong, check the error => 😥: n ${error}`,
foodUpdated: 0
})
}
}
module.exports = { addFood, getFood, deleteFood, updateFood }

但当我试图在Netlify应用程序中上传文件时,会出现以下错误:

错误:ENOENT:没有这样的文件或目录,打开'/app/controllers/..//client/public/uploads/9631bb96-e41d-4c9a-a35-d22b551ab662MASHAWI-IN-DUBAI.jpeg.webp'

我试过用谷歌搜索它很多次,但不幸的是没有找到解决方案。

谢谢你的帮助。

由两部分组成的答案:

  • 后端不需要将文件放入前端的目录结构中

    更好的选择可能是在后端项目中使用uploads/文件夹,通过HTTPS公开这些文件夹,并从前端链接到它们。

  • 但由于Heroku的文件系统短暂,这在Heroku上不起作用

    更好的选择是将它们保存到基于云的对象存储,如Amazon S3或Azure Blob Storage,或者更专业的服务,如Cloudinary,如果它们是图像。Heroku倾向于推荐S3。

    您的后端现在只需要存储每个文件的URL,并根据请求提供到前端的链接。

    即使在允许您将文件保存到后端文件系统的其他主机上,使用第三方服务也有许多好处。你可以简单地水平扩展(添加新节点(,你的应用程序变得不那么有状态,等等。

用户上传永远不属于您的代码库,无论您选择如何以及在哪里托管它们。它们是内容,而不是代码,不应与代码一起跟踪和版本化。

最新更新